merge_request_spec.rb 154 KB
Newer Older
1 2
# frozen_string_literal: true

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3 4
require 'spec_helper'

5
RSpec.describe MergeRequest, factory_default: :keep do
6
  include RepoHelpers
7
  include ProjectForksHelper
8
  include ReactiveCachingHelpers
9

10 11
  using RSpec::Parameterized::TableSyntax

12 13
  let_it_be(:namespace) { create_default(:namespace).freeze }
  let_it_be(:project, refind: true) { create_default(:project, :repository).freeze }
14

15 16
  subject { create(:merge_request) }

17
  describe 'associations' do
18 19
    subject { build_stubbed(:merge_request) }

20 21
    it { is_expected.to belong_to(:target_project).class_name('Project') }
    it { is_expected.to belong_to(:source_project).class_name('Project') }
22
    it { is_expected.to belong_to(:merge_user).class_name("User") }
23
    it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
24
    it { is_expected.to have_many(:reviewers).through(:merge_request_reviewers) }
25
    it { is_expected.to have_many(:merge_request_diffs) }
26
    it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
27
    it { is_expected.to belong_to(:milestone) }
28
    it { is_expected.to belong_to(:iteration) }
29 30
    it { is_expected.to have_many(:resource_milestone_events) }
    it { is_expected.to have_many(:resource_state_events) }
Patrick Bajao's avatar
Patrick Bajao committed
31 32
    it { is_expected.to have_many(:draft_notes) }
    it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
33
    it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
34 35 36 37 38 39 40 41 42 43 44 45 46 47

    context 'for forks' do
      let!(:project) { create(:project) }
      let!(:fork) { fork_project(project) }
      let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) }

      it 'does not load another project due to inverse relationship' do
        expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id)
      end

      it 'finds the associated merge request' do
        expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
      end
    end
48 49
  end

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  describe '.from_and_to_forks' do
    it 'returns only MRs from and to forks (with no internal MRs)' do
      project = create(:project)
      fork = fork_project(project)
      fork_2 = fork_project(project)
      mr_from_fork = create(:merge_request, source_project: fork, target_project: project)
      mr_to_fork = create(:merge_request, source_project: project, target_project: fork)

      create(:merge_request, source_project: fork, target_project: fork_2)
      create(:merge_request, source_project: project, target_project: project)

      expect(described_class.from_and_to_forks(project)).to contain_exactly(mr_from_fork, mr_to_fork)
    end
  end

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  describe '.order_merged_at_asc' do
    let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
    let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }

    it 'returns MRs ordered by merged_at ascending' do
      expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr])
    end
  end

  describe '.order_merged_at_desc' do
    let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
    let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }

    it 'returns MRs ordered by merged_at descending' do
      expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr])
    end
  end

83 84 85 86 87 88 89 90 91 92 93 94
  describe '.with_jira_issue_keys' do
    let_it_be(:mr_with_jira_title) { create(:merge_request, :unique_branches, title: 'Fix TEST-123') }
    let_it_be(:mr_with_jira_description) { create(:merge_request, :unique_branches, description: 'this closes TEST-321') }
    let_it_be(:mr_without_jira_reference) { create(:merge_request, :unique_branches) }

    subject { described_class.with_jira_issue_keys }

    it { is_expected.to contain_exactly(mr_with_jira_title, mr_with_jira_description) }

    it { is_expected.not_to include(mr_without_jira_reference) }
  end

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
  context 'scopes' do
    let_it_be(:user1) { create(:user) }
    let_it_be(:user2) { create(:user) }

    let_it_be(:merge_request1) { create(:merge_request, :unique_branches, reviewers: [user1])}
    let_it_be(:merge_request2) { create(:merge_request, :unique_branches, reviewers: [user2])}
    let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: [])}

    describe '.review_requested' do
      it 'returns MRs that has any review requests' do
        expect(described_class.review_requested).to eq([merge_request1, merge_request2])
      end
    end

    describe '.no_review_requested' do
      it 'returns MRs that has no review requests' do
        expect(described_class.no_review_requested).to eq([merge_request3])
      end
    end

    describe '.review_requested_to' do
      it 'returns MRs that the user has been requested to review' do
        expect(described_class.review_requested_to(user1)).to eq([merge_request1])
      end
    end

    describe '.no_review_requested_to' do
      it 'returns MRs that the user has been requested to review' do
        expect(described_class.no_review_requested_to(user1)).to eq([merge_request2, merge_request3])
      end
    end
  end

128
  describe '#squash_in_progress?' do
129 130 131
    let(:repo_path) do
      Gitlab::GitalyClient::StorageSettings.allow_disk_access do
        subject.source_project.repository.path
132
      end
133
    end
134

135
    let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") }
136

137 138 139
    before do
      system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master))
    end
140

141 142 143
    it 'returns true when there is a current squash directory' do
      expect(subject.squash_in_progress?).to be_truthy
    end
144

145 146
    it 'returns false when there is no squash directory' do
      FileUtils.rm_rf(squash_path)
147

148 149
      expect(subject.squash_in_progress?).to be_falsey
    end
150

151 152 153
    it 'returns false when the squash directory has expired' do
      time = 20.minutes.ago.to_time
      File.utime(time, time, squash_path)
154

155
      expect(subject.squash_in_progress?).to be_falsey
156 157
    end

158 159
    it 'returns false when the source project has been removed' do
      allow(subject).to receive(:source_project).and_return(nil)
160

161
      expect(subject.squash_in_progress?).to be_falsey
162 163 164 165 166
    end
  end

  describe '#squash?' do
    let(:merge_request) { build(:merge_request, squash: squash) }
167

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
    subject { merge_request.squash? }

    context 'disabled in database' do
      let(:squash) { false }

      it { is_expected.to be_falsy }
    end

    context 'enabled in database' do
      let(:squash) { true }

      it { is_expected.to be_truthy }
    end
  end

183 184
  describe '#default_squash_commit_message' do
    let(:project) { subject.project }
185 186 187
    let(:is_multiline) { -> (c) { c.description.present? } }
    let(:multiline_commits) { subject.commits.select(&is_multiline) }
    let(:singleline_commits) { subject.commits.reject(&is_multiline) }
188

189
    it 'returns the merge request title' do
190 191 192 193
      expect(subject.default_squash_commit_message).to eq(subject.title)
    end
  end

194 195 196 197 198 199 200
  describe 'modules' do
    subject { described_class }

    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) }
201 202
    it { is_expected.to include_module(MilestoneEventable) }
    it { is_expected.to include_module(StateEventable) }
203 204 205 206

    it_behaves_like 'AtomicInternalId' do
      let(:internal_id_attribute) { :iid }
      let(:instance) { build(:merge_request) }
207
      let(:scope) { :target_project }
208 209 210
      let(:scope_attrs) { { project: instance.target_project } }
      let(:usage) { :merge_requests }
    end
211 212 213
  end

  describe 'validation' do
214 215
    subject { build_stubbed(:merge_request) }

216 217
    it { is_expected.to validate_presence_of(:target_branch) }
    it { is_expected.to validate_presence_of(:source_branch) }
218

219
    context "Validation of merge user with Merge When Pipeline Succeeds" do
220 221 222 223 224
      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
225
        subject.merge_when_pipeline_succeeds = true
226 227 228 229
        expect(subject).not_to be_valid
      end

      it "is valid with merge user" do
230
        subject.merge_when_pipeline_succeeds = true
231 232 233 234 235
        subject.merge_user = build(:user)

        expect(subject).to be_valid
      end
    end
236

Mark Chao's avatar
Mark Chao committed
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
    context 'for branch' do
      before do
        stub_feature_flags(stricter_mr_branch_name: false)
      end

      where(:branch_name, :valid) do
        'foo' | true
        'foo:bar' | false
        '+foo:bar' | false
        'foo bar' | false
        '-foo' | false
        'HEAD' | true
        'refs/heads/master' | true
      end

      with_them do
        it "validates source_branch" do
          subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
          subject.valid?

          expect(subject.errors.added?(:source_branch)).to eq(!valid)
        end

        it "validates target_branch" do
          subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
          subject.valid?

          expect(subject.errors.added?(:target_branch)).to eq(!valid)
        end
      end
    end

269 270
    context 'for forks' do
      let(:project) { create(:project) }
271 272
      let(:fork1) { fork_project(project) }
      let(:fork2) { fork_project(project) }
273 274 275 276 277 278 279 280

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

        expect(subject).to be_valid
      end
    end
281 282
  end

283 284
  describe 'callbacks' do
    describe '#ensure_merge_request_metrics' do
285
      let(:merge_request) { create(:merge_request) }
286

287
      it 'creates metrics after saving' do
288 289 290 291 292 293 294 295 296
        expect(merge_request.metrics).to be_persisted
        expect(MergeRequest::Metrics.count).to eq(1)
      end

      it 'does not duplicate metrics for a merge request' do
        merge_request.mark_as_merged!

        expect(MergeRequest::Metrics.count).to eq(1)
      end
297 298 299 300 301 302 303 304 305 306 307 308 309

      it 'does not create duplicated metrics records when MR is concurrently updated' do
        merge_request.metrics.destroy

        instance1 = MergeRequest.find(merge_request.id)
        instance2 = MergeRequest.find(merge_request.id)

        instance1.ensure_metrics
        instance2.ensure_metrics

        metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
        expect(metrics_records.size).to eq(1)
      end
310 311 312 313 314 315 316 317 318

      it 'syncs the `target_project_id` to the metrics record' do
        project = create(:project)

        merge_request.update!(target_project: project, state: :closed)

        expect(merge_request.target_project_id).to eq(project.id)
        expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id)
      end
319 320 321
    end
  end

322
  describe 'respond to' do
323 324
    subject { build(:merge_request) }

325
    it { is_expected.to respond_to(:unchecked?) }
326
    it { is_expected.to respond_to(:checking?) }
327 328
    it { is_expected.to respond_to(:can_be_merged?) }
    it { is_expected.to respond_to(:cannot_be_merged?) }
329
    it { is_expected.to respond_to(:merge_params) }
330
    it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
331
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
332

333 334 335
  describe '.by_commit_sha' do
    subject(:by_commit_sha) { described_class.by_commit_sha(sha) }

336
    let!(:merge_request) { create(:merge_request) }
337 338 339 340 341 342 343 344 345 346 347 348 349 350

    context 'with sha contained in latest merge request diff' do
      let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }

      it 'returns merge requests' do
        expect(by_commit_sha).to eq([merge_request])
      end
    end

    context 'with sha contained not in latest merge request diff' do
      let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }

      it 'returns empty requests' do
        latest_merge_request_diff = merge_request.merge_request_diffs.create
351 352 353 354 355

        MergeRequestDiffCommit.where(
          merge_request_diff_id: latest_merge_request_diff,
          sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
        ).delete_all
356 357 358 359 360 361 362 363 364 365 366 367 368 369

        expect(by_commit_sha).to be_empty
      end
    end

    context 'with sha not contained in' do
      let(:sha) { 'b83d6e3' }

      it 'returns empty result' do
        expect(by_commit_sha).to be_empty
      end
    end
  end

370 371 372 373 374 375 376 377 378 379
  describe '.by_merge_commit_sha' do
    it 'returns merge requests that match the given merge commit' do
      mr = create(:merge_request, :merged, merge_commit_sha: '123abc')

      create(:merge_request, :merged, merge_commit_sha: '123def')

      expect(described_class.by_merge_commit_sha('123abc')).to eq([mr])
    end
  end

380 381 382 383 384 385 386 387 388 389 390
  describe '.by_squash_commit_sha' do
    subject { described_class.by_squash_commit_sha(sha) }

    let(:sha) { '123abc' }
    let(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) }

    it 'returns merge requests that match the given squash commit' do
      is_expected.to eq([merge_request])
    end
  end

391 392 393 394 395 396 397 398 399 400 401 402 403
  describe '.by_merge_or_squash_commit_sha' do
    subject { described_class.by_merge_or_squash_commit_sha([sha1, sha2]) }

    let(:sha1) { '123abc' }
    let(:sha2) { '456abc' }
    let(:mr1) { create(:merge_request, :merged, squash_commit_sha: sha1) }
    let(:mr2) { create(:merge_request, :merged, merge_commit_sha: sha2) }

    it 'returns merge requests that match the given squash and merge commits' do
      is_expected.to include(mr1, mr2)
    end
  end

404 405 406 407 408 409 410 411 412 413 414
  describe '.by_related_commit_sha' do
    subject { described_class.by_related_commit_sha(sha) }

    context 'when commit is a squash commit' do
      let!(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) }
      let(:sha) { '123abc' }

      it { is_expected.to eq([merge_request]) }
    end

    context 'when commit is a part of the merge request' do
415
      let!(:merge_request) { create(:merge_request) }
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
      let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }

      it { is_expected.to eq([merge_request]) }
    end

    context 'when commit is a merge commit' do
      let!(:merge_request) { create(:merge_request, :merged, merge_commit_sha: sha) }
      let(:sha) { '123abc' }

      it { is_expected.to eq([merge_request]) }
    end

    context 'when commit is not found' do
      let(:sha) { '0000' }

      it { is_expected.to be_empty }
    end
433 434

    context 'when commit is part of the merge request and a squash commit at the same time' do
435
      let!(:merge_request) { create(:merge_request) }
436 437 438 439 440 441 442 443
      let(:sha) { merge_request.commits.first.id }

      before do
        merge_request.update!(squash_commit_sha: sha)
      end

      it { is_expected.to eq([merge_request]) }
    end
444 445
  end

446 447 448 449 450 451
  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

452 453 454 455 456 457 458 459 460 461 462
  describe '.set_latest_merge_request_diff_ids!' do
    def create_merge_request_with_diffs(source_branch, diffs: 2)
      params = {
        target_project: project,
        target_branch: 'master',
        source_project: project,
        source_branch: source_branch
      }

      create(:merge_request, params).tap do |mr|
        diffs.times { mr.merge_request_diffs.create }
463
        mr.create_merge_head_diff
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
      end
    end

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

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

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

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

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

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  describe '.recent_target_branches' do
    let(:project) { create(:project) }
    let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
    let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
    let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
    let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }

    before do
      merge_request1.update_columns(updated_at: 1.day.since)
      merge_request2.update_columns(updated_at: 2.days.since)
      merge_request3.update_columns(updated_at: 3.days.since)
      merge_request4.update_columns(updated_at: 4.days.since)
    end

    it 'returns target branches sort by updated at desc' do
505
      expect(described_class.recent_target_branches).to match_array(%w[feature merge-test fix])
506 507 508
    end
  end

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
  describe '.sort_by_attribute' do
    context 'merged_at' do
      let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
      let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }

      it 'sorts asc' do
        merge_requests = described_class.sort_by_attribute(:merged_at_asc)
        expect(merge_requests).to eq([older_mr, newer_mr])
      end

      it 'sorts desc' do
        merge_requests = described_class.sort_by_attribute(:merged_at_desc)
        expect(merge_requests).to eq([newer_mr, older_mr])
      end
    end
  end

526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
  describe 'time to merge calculations' do
    let_it_be(:user) { create(:user) }
    let_it_be(:project) { create(:project) }

    let!(:mr1) do
      create(
        :merge_request,
        :with_merged_metrics,
        source_project: project,
        target_project: project
      )
    end

    let!(:mr2) do
      create(
        :merge_request,
        :with_merged_metrics,
        source_project: project,
        target_project: project
      )
    end

    let!(:mr3) do
      create(
        :merge_request,
        :with_merged_metrics,
        source_project: project,
        target_project: project
      )
    end

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

    before do
      project.add_user(user, :developer)
    end

    describe '.total_time_to_merge' do
      it 'returns the sum of the time to merge for all merged MRs' do
        mrs = project.merge_requests

        expect(mrs.total_time_to_merge).to be_within(1).of(expected_total_time(mrs))
      end

      context 'when merged_at is earlier than created_at' do
        before do
          mr1.metrics.update!(merged_at: mr1.metrics.created_at - 1.week)
        end

        it 'returns nil' do
          mrs = project.merge_requests.where(id: mr1.id)

          expect(mrs.total_time_to_merge).to be_nil
        end
      end

      def expected_total_time(mrs)
        mrs = mrs.reject { |mr| mr.merged_at.nil? }
        mrs.reduce(0.0) do |sum, mr|
          (mr.merged_at - mr.created_at) + sum
        end
      end
    end
  end

597
  describe '#target_branch_sha' do
598
    let(:project) { create(:project, :repository) }
599

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

602
    context 'when the target branch does not exist' do
603
      before do
604
        project.repository.rm_branch(subject.author, subject.target_branch)
605
        subject.clear_memoized_shas
606
      end
607 608

      it 'returns nil' do
609
        expect(subject.target_branch_sha).to be_nil
610 611
      end
    end
612 613 614 615 616 617

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

      expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
    end
618 619
  end

620 621 622
  describe '#card_attributes' do
    it 'includes the author name' do
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
623
      allow(subject).to receive(:assignees).and_return([])
624

625
      expect(subject.card_attributes)
626
        .to eq({ 'Author' => 'Robert', 'Assignee' => "" })
627 628
    end

629
    it 'includes the assignees name' do
630
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
631
      allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
632

633
      expect(subject.card_attributes)
634
        .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
635 636 637
    end
  end

638 639 640 641
  describe '#assignee_or_author?' do
    let(:user) { create(:user) }

    it 'returns true for a user that is assigned to a merge request' do
642
      subject.assignees = [user]
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657

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

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

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

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

658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
  describe '#visible_closing_issues_for' do
    let(:guest) { create(:user) }
    let(:developer) { create(:user) }
    let(:issue_1) { create(:issue, project: subject.source_project) }
    let(:issue_2) { create(:issue, project: subject.source_project) }
    let(:confidential_issue) { create(:issue, :confidential, project: subject.source_project) }

    before do
      subject.project.add_developer(subject.author)
      subject.target_branch = subject.project.default_branch
      commit = double('commit1', safe_message: "Fixes #{issue_1.to_reference} #{issue_2.to_reference} #{confidential_issue.to_reference}")
      allow(subject).to receive(:commits).and_return([commit])
    end

    it 'shows only allowed issues to guest' do
      subject.project.add_guest(guest)

      subject.cache_merge_request_closes_issues!

      expect(subject.visible_closing_issues_for(guest)).to match_array([issue_1, issue_2])
    end

    it 'shows only allowed issues to developer' do
      subject.project.add_developer(developer)

      subject.cache_merge_request_closes_issues!

      expect(subject.visible_closing_issues_for(developer)).to match_array([issue_1, confidential_issue, issue_2])
    end

    context 'when external issue tracker is enabled' do
689 690 691 692
      let(:project) { create(:project, :repository) }

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

693 694 695 696 697 698 699 700 701 702 703 704 705 706
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.save!
      end

      it 'calls non #closes_issues to retrieve data' do
        expect(subject).to receive(:closes_issues)
        expect(subject).not_to receive(:cached_closes_issues)

        subject.visible_closing_issues_for
      end
    end
  end

707 708
  describe '#cache_merge_request_closes_issues!' do
    before do
709
      subject.project.add_developer(subject.author)
710 711 712 713 714 715 716 717
      subject.target_branch = subject.project.default_branch
    end

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

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

721 722 723 724 725
    it 'does not cache closed issues when merge request is closed' do
      issue  = create :issue, project: subject.project
      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")

      allow(subject).to receive(:commits).and_return([commit])
726
      allow(subject).to receive(:state_id).and_return(described_class.available_states[:closed])
727 728 729 730 731 732 733 734

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

    it 'does not cache closed issues when merge request is merged' do
      issue  = create :issue, project: subject.project
      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
      allow(subject).to receive(:commits).and_return([commit])
735
      allow(subject).to receive(:state_id).and_return(described_class.available_states[:merged])
736 737 738 739

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

740 741
    context 'when both internal and external issue trackers are enabled' do
      before do
742
        create(:jira_service, project: subject.project)
743
        subject.project.reload
744 745 746 747 748 749
      end

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

751
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
752 753 754 755 756 757 758 759 760 761 762 763 764 765
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
      end

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

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

    context 'when only external issue tracker enabled' do
766 767 768 769
      let(:project) { create(:project, :repository) }

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

770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.issues_enabled = false
        subject.project.save!
      end

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

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

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

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

795
  describe '#source_branch_sha' do
796
    let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
797 798

    context 'with diffs' do
799
      subject { create(:merge_request) }
800

801
      it 'returns the sha of the source branch last commit' do
802
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
803 804 805
      end
    end

806 807
    context 'without diffs' do
      subject { create(:merge_request, :without_diffs) }
808

809
      it 'returns the sha of the source branch last commit' do
810
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
811
      end
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826

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

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

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

          subject.source_project.repository.rm_tag(subject.author, tag_name)
        end
      end
827 828
    end

829 830
    context 'when the merge request is being created' do
      subject { build(:merge_request, source_branch: nil, compare_commits: []) }
831

832
      it 'returns nil' do
833
        expect(subject.source_branch_sha).to be_nil
834 835
      end
    end
836 837 838 839 840 841

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

      expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
    end
842 843
  end

844
  describe '#to_reference' do
845
    let(:project) { build(:project, name: 'sample-project') }
846 847
    let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }

848
    it 'returns a String reference to the object' do
849
      expect(merge_request.to_reference).to eq "!1"
850 851 852
    end

    it 'supports a cross-project reference' do
853
      another_project = build(:project, name: 'another-project', namespace: project.namespace)
854
      expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
855
    end
856 857

    it 'returns a String reference with the full path' do
858
      expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
859
    end
860
  end
861

862
  describe '#raw_diffs' do
863 864 865
    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }

    context 'when there are MR diffs' do
866
      let(:merge_request) { create(:merge_request) }
867

868
      it 'delegates to the MR diffs' do
869
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
870

871 872 873 874 875
        merge_request.raw_diffs(options)
      end
    end

    context 'when there are no MR diffs' do
876 877
      let(:merge_request) { build(:merge_request) }

878 879 880 881 882 883 884 885 886 887
      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

888 889 890 891 892 893
  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
894
        merge_request.save
895

896
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
897

898
        merge_request.diffs(options).diff_files
899 900 901 902
      end
    end

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

906
        expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
907 908 909 910 911 912

        merge_request.diffs(options)
      end
    end
  end

913
  describe '#note_positions_for_paths' do
Patrick Bajao's avatar
Patrick Bajao committed
914
    let(:user) { create(:user) }
915
    let(:merge_request) { create(:merge_request) }
916 917 918 919
    let(:project) { merge_request.project }
    let!(:diff_note) do
      create(:diff_note_on_merge_request, project: project, noteable: merge_request)
    end
920

Patrick Bajao's avatar
Patrick Bajao committed
921 922 923
    let!(:draft_note) do
      create(:draft_note_on_text_diff, author: user, merge_request: merge_request)
    end
924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955

    let(:file_paths) { merge_request.diffs.diff_files.map(&:file_path) }

    subject do
      merge_request.note_positions_for_paths(file_paths)
    end

    it 'returns a Gitlab::Diff::PositionCollection' do
      expect(subject).to be_a(Gitlab::Diff::PositionCollection)
    end

    context 'within all diff files' do
      it 'returns correct positions' do
        expect(subject).to match_array([diff_note.position])
      end
    end

    context 'within specific diff file' do
      let(:file_paths) { [diff_note.position.file_path] }

      it 'returns correct positions' do
        expect(subject).to match_array([diff_note.position])
      end
    end

    context 'within no diff files' do
      let(:file_paths) { [] }

      it 'returns no positions' do
        expect(subject.to_a).to be_empty
      end
    end
Patrick Bajao's avatar
Patrick Bajao committed
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975

    context 'when user is given' do
      subject do
        merge_request.note_positions_for_paths(file_paths, user)
      end

      it 'returns notes and draft notes positions' do
        expect(subject).to match_array([draft_note.position, diff_note.position])
      end
    end

    context 'when user is not given' do
      subject do
        merge_request.note_positions_for_paths(file_paths)
      end

      it 'returns notes positions' do
        expect(subject).to match_array([diff_note.position])
      end
    end
976 977
  end

978
  describe '#discussions_diffs' do
979 980
    let(:merge_request) { create(:merge_request) }

981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
    shared_examples 'discussions diffs collection' do
      it 'initializes Gitlab::DiscussionsDiff::FileCollection with correct data' do
        note_diff_file = diff_note.note_diff_file

        expect(Gitlab::DiscussionsDiff::FileCollection)
          .to receive(:new)
          .with([note_diff_file])
          .and_call_original

        result = merge_request.discussions_diffs

        expect(result).to be_a(Gitlab::DiscussionsDiff::FileCollection)
      end

      it 'eager loads relations' do
        result = merge_request.discussions_diffs

        recorder = ActiveRecord::QueryRecorder.new do
          result.first.diff_note
          result.first.diff_note.project
        end

        expect(recorder.count).to be_zero
      end
    end

1007
    context 'with commit diff note' do
1008
      let(:other_merge_request) { create(:merge_request, source_project: create(:project, :repository)) }
1009 1010 1011 1012 1013 1014 1015 1016 1017

      let!(:diff_note) do
        create(:diff_note_on_commit, project: merge_request.project)
      end

      let!(:other_mr_diff_note) do
        create(:diff_note_on_commit, project: other_merge_request.project)
      end

1018
      it_behaves_like 'discussions diffs collection'
1019 1020 1021
    end

    context 'with merge request diff note' do
1022
      let!(:diff_note) do
1023 1024 1025
        create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request)
      end

1026
      it_behaves_like 'discussions diffs collection'
1027 1028 1029
    end
  end

1030
  describe '#diff_size' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1031 1032
    let_it_be(:project) { create(:project, :repository) }

1033
    let(:merge_request) do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1034
      build(:merge_request, source_project: project, source_branch: 'expand-collapse-files', target_branch: 'master')
1035 1036 1037
    end

    context 'when there are MR diffs' do
1038
      it 'returns the correct count' do
1039
        merge_request.save
1040 1041

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

1044 1045 1046 1047 1048
      it 'returns the correct overflow count' do
        allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
        merge_request.save

        expect(merge_request.diff_size).to eq('2+')
1049 1050 1051
      end

      it 'does not perform highlighting' do
1052 1053
        merge_request.save

1054 1055 1056 1057 1058 1059 1060
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end

    context 'when there are no MR diffs' do
1061
      def set_compare(merge_request)
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
        merge_request.compare = CompareService.new(
          merge_request.source_project,
          merge_request.source_branch
        ).execute(
          merge_request.target_project,
          merge_request.target_branch
        )
      end

      it 'returns the correct count' do
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
        set_compare(merge_request)

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

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

        expect(merge_request.diff_size).to eq('2+')
1082 1083 1084
      end

      it 'does not perform highlighting' do
1085 1086
        set_compare(merge_request)

1087 1088 1089 1090 1091 1092 1093
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end
  end

1094 1095
  describe '#modified_paths' do
    let(:paths) { double(:paths) }
1096

1097 1098 1099
    subject(:merge_request) { build(:merge_request) }

    before do
1100
      allow(diff).to receive(:modified_paths).and_return(paths)
1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
    end

    context 'when past_merge_request_diff is specified' do
      let(:another_diff) { double(:merge_request_diff) }
      let(:diff) { another_diff }

      it 'returns affected file paths from specified past_merge_request_diff' do
        expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths)
      end
    end

    context 'when compare is present' do
      let(:compare) { double(:compare) }
      let(:diff) { compare }

1116
      before do
1117 1118
        merge_request.compare = compare

1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
        expect(merge_request).to receive(:diff_stats).and_return(diff_stats)
      end

      context 'and diff_stats are not present' do
        let(:diff_stats) { nil }

        it 'returns affected file paths from compare' do
          expect(merge_request.modified_paths).to eq(paths)
        end
      end

      context 'and diff_stats are present' do
        let(:diff_stats) { double(:diff_stats) }

        it 'returns affected file paths from compare' do
          diff_stats_path = double(:diff_stats_paths)
          expect(diff_stats).to receive(:paths).and_return(diff_stats_path)

          expect(merge_request.modified_paths).to eq(diff_stats_path)
        end
1139 1140 1141 1142 1143
      end
    end

    context 'when no arguments provided' do
      let(:diff) { merge_request.merge_request_diff }
1144

1145 1146 1147 1148 1149 1150 1151 1152
      subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }

      it 'returns affected file paths for merge_request_diff' do
        expect(merge_request.modified_paths).to eq(paths)
      end
    end
  end

1153 1154 1155 1156 1157 1158 1159 1160 1161 1162
  describe '#new_paths' do
    let(:merge_request) do
      create(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
    end

    it 'returns new path of changed files' do
      expect(merge_request.new_paths.count).to eq(105)
    end
  end

1163
  describe "#related_notes" do
1164
    let!(:merge_request) { create(:merge_request) }
1165 1166

    before do
1167
      allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
1168 1169
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.project)
1170
      create(:note, noteable: merge_request, project: merge_request.project)
1171 1172
    end

1173
    it "includes notes for commits" do
1174
      expect(merge_request.commits).not_to be_empty
1175
      expect(merge_request.related_notes.count).to eq(2)
1176
    end
1177

1178
    it "includes notes for commits from target project as well" do
1179 1180 1181
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.target_project)

1182
      expect(merge_request.commits).not_to be_empty
1183
      expect(merge_request.related_notes.count).to eq(3)
1184
    end
1185 1186 1187 1188 1189 1190 1191 1192

    it "excludes system notes for commits" do
      system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id,
                                                     project: merge_request.project)

      expect(merge_request.related_notes.count).to eq(2)
      expect(merge_request.related_notes).not_to include(system_note)
    end
1193
  end
1194

1195 1196
  describe '#for_fork?' do
    it 'returns true if the merge request is for a fork' do
1197 1198
      subject.source_project = build_stubbed(:project, namespace: create(:group))
      subject.target_project = build_stubbed(:project, namespace: create(:group))
1199

1200
      expect(subject.for_fork?).to be_truthy
1201
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1202

1203
    it 'returns false if is not for a fork' do
1204
      expect(subject.for_fork?).to be_falsey
1205 1206 1207
    end
  end

1208
  describe '#closes_issues' do
1209 1210
    let(:project) { create(:project) }

1211 1212
    let(:issue0) { create :issue, project: subject.project }
    let(:issue1) { create :issue, project: subject.project }
1213 1214 1215 1216

    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}") }
1217

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

1220
    before do
1221
      subject.project.add_developer(subject.author)
1222
      allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
1223 1224
    end

1225
    it 'accesses the set of issues that will be closed on acceptance' do
1226 1227
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
1228

1229
      closed = subject.closes_issues
1230

1231 1232
      expect(closed).to include(issue0, issue1)
    end
1233

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

1238
      expect(subject.closes_issues).to be_empty
1239
    end
1240 1241 1242 1243 1244 1245 1246 1247 1248

    it 'ignores referenced issues when auto-close is disabled' do
      subject.project.update!(autoclose_referenced_issues: false)

      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)

      expect(subject.closes_issues).to be_empty
    end
1249
  end
1250

1251
  describe '#issues_mentioned_but_not_closing' do
1252 1253 1254
    let(:closing_issue) { create :issue, project: subject.project }
    let(:mentioned_issue) { create :issue, project: subject.project }
    let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
1255

1256
    it 'detects issues mentioned in description but not closed' do
1257
      subject.project.add_developer(subject.author)
1258
      subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
1259

1260
      allow(subject).to receive(:commits).and_return([commit])
1261 1262
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
1263
      subject.cache_merge_request_closes_issues!
1264

1265
      expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
1266
    end
1267 1268 1269

    context 'when the project has an external issue tracker' do
      before do
1270
        subject.project.add_developer(subject.author)
1271 1272 1273
        commit = double(:commit, safe_message: 'Fixes TEST-3')

        create(:jira_service, project: subject.project)
1274
        subject.project.reload
1275 1276 1277 1278 1279 1280 1281

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

      it 'detects issues mentioned in description but not closed' do
1282 1283
        subject.cache_merge_request_closes_issues!

1284 1285 1286
        expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
      end
    end
1287 1288
  end

1289
  describe "#work_in_progress?" do
1290 1291 1292
    subject { build_stubbed(:merge_request) }

    [
1293 1294
      'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP: [WIP] WIP:',
      'draft:', 'Draft: ', '[Draft]', '[DRAFT] ', 'Draft - '
1295
    ].each do |wip_prefix|
1296 1297
      it "detects the '#{wip_prefix}' prefix" do
        subject.title = "#{wip_prefix}#{subject.title}"
1298

1299
        expect(subject.work_in_progress?).to eq true
1300
      end
1301 1302
    end

1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314
    it "detects merge request title just saying 'wip'" do
      subject.title = "wip"

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

    it "detects merge request title just saying 'draft'" do
      subject.title = "draft"

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

1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326
    it 'does not detect WIP in the middle of the title' do
      subject.title = 'Something with WIP in the middle'

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

    it 'does not detect Draft in the middle of the title' do
      subject.title = 'Something with Draft in the middle'

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

1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344
    it 'does not detect Draft: in the middle of the title' do
      subject.title = 'Something with Draft: in the middle'

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

    it 'does not detect WIP at the end of the title' do
      subject.title = 'Something ends with WIP'

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

    it 'does not detect Draft at the end of the title' do
      subject.title = 'Something ends with Draft'

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

1345 1346
    it "doesn't detect WIP for words starting with WIP" do
      subject.title = "Wipwap #{subject.title}"
1347
      expect(subject.work_in_progress?).to eq false
1348 1349
    end

1350 1351
    it "doesn't detect WIP for words containing with WIP" do
      subject.title = "WupWipwap #{subject.title}"
1352
      expect(subject.work_in_progress?).to eq false
1353 1354
    end

1355 1356 1357 1358 1359
    it "doesn't detect draft for words containing with draft" do
      subject.title = "Drafting #{subject.title}"
      expect(subject.work_in_progress?).to eq false
    end

1360
    it "doesn't detect WIP by default" do
1361
      expect(subject.work_in_progress?).to eq false
1362
    end
1363 1364 1365 1366

    it "is aliased to #draft?" do
      expect(subject.method(:work_in_progress?)).to eq(subject.method(:draft?))
    end
1367 1368
  end

1369
  describe "#wipless_title" do
1370 1371 1372
    subject { build_stubbed(:merge_request) }

    [
1373 1374
      'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP: [WIP] WIP:',
      'draft:', 'Draft: ', '[Draft]', '[DRAFT] ', 'Draft - '
1375
    ].each do |wip_prefix|
1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389
      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
1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425

    it 'removes only WIP prefix from the MR title' do
      subject.title = 'WIP: Implement feature called WIP'

      expect(subject.wipless_title).to eq 'Implement feature called WIP'
    end

    it 'removes only draft prefix from the MR title' do
      subject.title = 'Draft: Implement feature called draft'

      expect(subject.wipless_title).to eq 'Implement feature called draft'
    end

    it 'does not remove WIP in the middle of the title' do
      subject.title = 'Something with WIP in the middle'

      expect(subject.wipless_title).to eq subject.title
    end

    it 'does not remove Draft in the middle of the title' do
      subject.title = 'Something with Draft in the middle'

      expect(subject.wipless_title).to eq subject.title
    end

    it 'does not remove WIP at the end of the title' do
      subject.title = 'Something ends with WIP'

      expect(subject.wipless_title).to eq subject.title
    end

    it 'does not remove Draft at the end of the title' do
      subject.title = 'Something ends with Draft'

      expect(subject.wipless_title).to eq subject.title
    end
1426 1427 1428
  end

  describe "#wip_title" do
1429 1430
    it "adds the Draft: prefix to the title" do
      wip_title = "Draft: #{subject.title}"
1431 1432

      expect(subject.wip_title).to eq wip_title
1433
    end
1434

1435 1436
    it "does not add the Draft: prefix multiple times" do
      wip_title = "Draft: #{subject.title}"
1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449
      subject.title = subject.wip_title
      subject.title = subject.wip_title

      expect(subject.wip_title).to eq wip_title
    end

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

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

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1450
  describe '#can_remove_source_branch?' do
1451 1452
    let_it_be(:user) { create(:user) }
    let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) }
1453

1454
    subject { merge_request }
1455

1456
    before do
1457
      subject.source_project.add_maintainer(user)
1458
    end
1459

1460
    it "can't be removed when its a protected branch" do
1461
      allow(ProtectedBranch).to receive(:protected?).and_return(true)
1462

1463 1464 1465
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

1466 1467 1468 1469 1470 1471
    it "can't be removed because source project has been deleted" do
      subject.source_project = nil

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

1472
    it "can't remove a root ref" do
1473
      subject.update(source_branch: 'master', target_branch: 'feature')
1474 1475 1476 1477

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

1478
    it "is unable to remove the source branch for a project the user cannot push to" do
1479 1480
      user2 = create(:user)

1481 1482 1483
      expect(subject.can_remove_source_branch?(user2)).to be_falsey
    end

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

1487
      expect(subject.can_remove_source_branch?(user)).to be_truthy
1488
    end
1489 1490

    it "cannot be removed if the last commit is not also the head of the source branch" do
1491
      subject.clear_memoized_shas
1492 1493
      subject.source_branch = "lfs"

1494 1495
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
1496 1497
  end

1498
  describe "#source_branch_exists?" do
1499 1500
    let(:project) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, source_project: project) }
1501 1502
    let(:repository) { merge_request.source_project.repository }

1503
    context 'when the source project is set' do
1504 1505
      it 'returns true when the branch exists' do
        expect(merge_request.source_branch_exists?).to eq(true)
1506 1507 1508
      end
    end

1509
    context 'when the source project is not set' do
1510
      before do
1511
        merge_request.source_project = nil
1512 1513
      end

1514 1515
      it 'returns false' do
        expect(merge_request.source_branch_exists?).to eq(false)
1516 1517 1518 1519
      end
    end
  end

1520
  describe '#default_merge_commit_message' do
1521 1522 1523
    it 'includes merge information as the title' do
      request = build(:merge_request, source_branch: 'source', target_branch: 'target')

1524
      expect(request.default_merge_commit_message)
1525 1526 1527 1528 1529 1530
        .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')

1531
      expect(request.default_merge_commit_message)
1532 1533 1534
        .to match("Remove all technical debt\n\n")
    end

1535
    it 'includes its closed issues in the body' do
1536
      issue = create(:issue, project: subject.project)
1537

1538
      subject.project.add_developer(subject.author)
1539
      subject.description = "This issue Closes #{issue.to_reference}"
1540 1541
      allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
      subject.cache_merge_request_closes_issues!
1542

1543
      expect(subject.default_merge_commit_message)
1544
        .to match("Closes #{issue.to_reference}")
1545 1546 1547 1548 1549
    end

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

1550
      expect(request.default_merge_commit_message)
1551
        .to match("See merge request #{request.to_reference(full: true)}")
1552 1553 1554 1555 1556
    end

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

1557
      expect(request.default_merge_commit_message).not_to match("Title\n\n\n\n")
1558
    end
1559 1560 1561 1562

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

1563
      expect(request.default_merge_commit_message(include_description: true))
1564 1565 1566 1567 1568 1569
        .to match("By removing all code\n\n")
    end

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

1570
      expect(request.default_merge_commit_message)
1571 1572
        .not_to match("By removing all code\n\n")
    end
1573 1574
  end

1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588
  describe "#auto_merge_strategy" do
    subject { merge_request.auto_merge_strategy }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

    it { is_expected.to eq('merge_when_pipeline_succeeds') }

    context 'when auto merge is disabled' do
      let(:merge_request) { create(:merge_request) }

      it { is_expected.to be_nil }
    end
  end

1589 1590 1591
  describe '#committers' do
    it 'returns all the committers of every commit in the merge request' do
      users = subject.commits.without_merge_commits.map(&:committer_email).uniq.map do |email|
1592 1593 1594
        create(:user, email: email)
      end

1595
      expect(subject.committers).to match_array(users)
1596 1597
    end

1598 1599
    it 'returns an empty array if no committer is associated with a user' do
      expect(subject.committers).to be_empty
1600 1601 1602
    end
  end

1603
  describe '#hook_attrs' do
1604 1605 1606 1607 1608 1609 1610 1611
    it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
      builder = double

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

      subject.hook_attrs
1612
    end
1613 1614 1615
  end

  describe '#diverged_commits_count' do
1616
    let(:project) { create(:project, :repository) }
1617
    let(:forked_project) { fork_project(project, nil, repository: true) }
1618

1619
    context 'when the target branch does not exist anymore' do
1620 1621 1622 1623
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
1624
        subject.clear_memoized_shas
1625
      end
1626 1627

      it 'does not crash' do
1628
        expect { subject.diverged_commits_count }.not_to raise_error
1629 1630 1631 1632 1633 1634 1635
      end

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

1636 1637 1638 1639
    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
1640
        expect(subject.diverged_commits_count).to eq(29)
1641 1642 1643 1644
      end
    end

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

1647
      it 'counts commits that are on target branch but not on source branch', :sidekiq_might_not_need_inline do
1648
        expect(subject.diverged_commits_count).to eq(29)
1649 1650 1651 1652
      end
    end

    context 'rebased on fork' do
1653
      subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
1654 1655 1656 1657 1658 1659 1660

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

      it 'caches the output' do
1666 1667 1668
        expect(subject).to receive(:compute_diverged_commits_count)
          .once
          .and_return(2)
1669 1670 1671 1672 1673 1674

        subject.diverged_commits_count
        subject.diverged_commits_count
      end

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

        subject.diverged_commits_count
1680
        allow(subject).to receive(:source_branch_sha).and_return('123abc')
1681 1682 1683 1684
        subject.diverged_commits_count
      end

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

        subject.diverged_commits_count
1690
        allow(subject).to receive(:target_branch_sha).and_return('123abc')
1691 1692 1693
        subject.diverged_commits_count
      end
    end
1694 1695
  end

1696
  it_behaves_like 'an editable mentionable' do
1697
    subject { create(:merge_request, :simple, source_project: create(:project, :repository)) }
1698

1699
    let(:backref_text) { "merge request #{subject.to_reference}" }
1700
    let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
1701
  end
Vinnie Okada's avatar
Vinnie Okada committed
1702 1703

  it_behaves_like 'a Taskable' do
1704
    subject { create :merge_request, :simple }
Vinnie Okada's avatar
Vinnie Okada committed
1705
  end
1706

1707
  describe '#commit_shas' do
1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721
    context 'persisted merge request' do
      context 'with a limit' do
        it 'returns a limited number of commit shas' do
          expect(subject.commit_shas(limit: 2)).to eq(%w[
            b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
          ])
        end
      end

      context 'without a limit' do
        it 'returns all commit shas of the merge request diff' do
          expect(subject.commit_shas.size).to eq(29)
        end
      end
1722 1723
    end

1724
    context 'new merge request' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
1725 1726 1727
      let_it_be(:project) { create(:project, :repository) }

      subject { build(:merge_request, source_project: project) }
1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752

      context 'compare commits' do
        before do
          subject.compare_commits = [
            double(sha: 'sha1'), double(sha: 'sha2')
          ]
        end

        context 'without a limit' do
          it 'returns all shas of compare commits' do
            expect(subject.commit_shas).to eq(%w[sha2 sha1])
          end
        end

        context 'with a limit' do
          it 'returns a limited number of shas' do
            expect(subject.commit_shas(limit: 1)).to eq(['sha2'])
          end
        end
      end

      it 'returns diff_head_sha as an array' do
        expect(subject.commit_shas).to eq([subject.diff_head_sha])
        expect(subject.commit_shas(limit: 2)).to eq([subject.diff_head_sha])
      end
1753 1754 1755
    end
  end

1756
  context 'head pipeline' do
1757 1758
    let(:diff_head_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }

1759
    before do
1760
      allow(subject).to receive(:diff_head_sha).and_return(diff_head_sha)
1761
    end
1762

1763 1764 1765
    describe '#head_pipeline' do
      it 'returns nil for MR without head_pipeline_id' do
        subject.update_attribute(:head_pipeline_id, nil)
1766

1767 1768
        expect(subject.head_pipeline).to be_nil
      end
1769 1770 1771 1772 1773 1774 1775 1776

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

          expect(subject.head_pipeline).to be_nil
        end
      end
1777 1778
    end

1779
    describe '#actual_head_pipeline' do
1780 1781 1782
      it 'returns nil for MR with old pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
        subject.update_attribute(:head_pipeline_id, pipeline.id)
1783

1784
        expect(subject.actual_head_pipeline).to be_nil
1785
      end
1786

1787
      it 'returns the pipeline for MR with recent pipeline' do
1788 1789 1790 1791 1792 1793 1794 1795 1796
        pipeline = create(:ci_empty_pipeline, sha: diff_head_sha)
        subject.update_attribute(:head_pipeline_id, pipeline.id)

        expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
        expect(subject.actual_head_pipeline).to eq(pipeline)
      end

      it 'returns the pipeline for MR with recent merge request pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'merge-sha', source_sha: diff_head_sha)
1797
        subject.update_attribute(:head_pipeline_id, pipeline.id)
1798

1799 1800
        expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
        expect(subject.actual_head_pipeline).to eq(pipeline)
1801
      end
1802 1803 1804 1805 1806 1807

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

        expect(subject.actual_head_pipeline).to be_nil
      end
1808 1809
    end
  end
Yorick Peterse's avatar
Yorick Peterse committed
1810

1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823
  describe '#merge_pipeline' do
    it 'returns nil when not merged' do
      expect(subject.merge_pipeline).to be_nil
    end

    context 'when the MR is merged' do
      let(:sha)      { subject.target_project.commit.id }
      let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }

      before do
        subject.mark_as_merged!
      end

1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851
      context 'and there is a merge commit' do
        before do
          subject.update_attribute(:merge_commit_sha, pipeline.sha)
        end

        it 'returns the pipeline associated with that merge request' do
          expect(subject.merge_pipeline).to eq(pipeline)
        end
      end

      context 'and there is no merge commit, but there is a diff head' do
        before do
          allow(subject).to receive(:diff_head_sha).and_return(pipeline.sha)
        end

        it 'returns the pipeline associated with that merge request' do
          expect(subject.merge_pipeline).to eq(pipeline)
        end
      end

      context 'and there is no merge commit, but there is a squash commit' do
        before do
          subject.update_attribute(:squash_commit_sha, pipeline.sha)
        end

        it 'returns the pipeline associated with that merge request' do
          expect(subject.merge_pipeline).to eq(pipeline)
        end
1852 1853 1854 1855
      end
    end
  end

1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898
  describe '#has_ci?' do
    let(:merge_request) { build_stubbed(:merge_request) }

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

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

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

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

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

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

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

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

1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916
  describe '#update_head_pipeline' do
    subject { merge_request.update_head_pipeline }

    let(:merge_request) { create(:merge_request) }

    context 'when there is a pipeline with the diff head sha' do
      let!(:pipeline) do
        create(:ci_empty_pipeline,
               project: merge_request.project,
               sha: merge_request.diff_head_sha,
               ref: merge_request.source_branch)
      end

      it 'updates the head pipeline' do
        expect { subject }
          .to change { merge_request.reload.head_pipeline }
          .from(nil).to(pipeline)
      end
1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933

      context 'when merge request has already had head pipeline' do
        before do
          merge_request.update!(head_pipeline: pipeline)
        end

        context 'when failed to find an actual head pipeline' do
          before do
            allow(merge_request).to receive(:find_actual_head_pipeline) { }
          end

          it 'does not update the current head pipeline' do
            expect { subject }
              .not_to change { merge_request.reload.head_pipeline }
          end
        end
      end
1934 1935
    end

1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966
    context 'when detached merge request pipeline is run on head ref of the merge request' do
      let!(:pipeline) do
        create(:ci_pipeline,
               source: :merge_request_event,
               project: merge_request.source_project,
               ref: merge_request.ref_path,
               sha: sha,
               merge_request: merge_request)
      end

      let(:sha) { merge_request.diff_head_sha }

      it 'sets the head ref of the merge request to the pipeline ref' do
        expect(pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
      end

      it 'updates correctly even though the target branch name of the merge request is different from the pipeline ref' do
        expect { subject }
          .to change { merge_request.reload.head_pipeline }
          .from(nil).to(pipeline)
      end

      context 'when sha is not HEAD of the source branch' do
        let(:sha) { merge_request.diff_base_sha }

        it 'does not update head pipeline' do
          expect { subject }.not_to change { merge_request.reload.head_pipeline }
        end
      end
    end

1967 1968 1969 1970 1971 1972 1973 1974
    context 'when there are no pipelines with the diff head sha' do
      it 'does not update the head pipeline' do
        expect { subject }
          .not_to change { merge_request.reload.head_pipeline }
      end
    end
  end

1975 1976 1977 1978
  describe '#has_test_reports?' do
    subject { merge_request.has_test_reports? }

    context 'when head pipeline has test reports' do
1979
      let(:merge_request) { create(:merge_request, :with_test_reports) }
1980 1981 1982 1983 1984

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have test reports' do
1985
      let(:merge_request) { create(:merge_request) }
1986 1987 1988 1989 1990

      it { is_expected.to be_falsey }
    end
  end

1991 1992 1993 1994
  describe '#has_accessibility_reports?' do
    subject { merge_request.has_accessibility_reports? }

    context 'when head pipeline has an accessibility reports' do
1995
      let(:merge_request) { create(:merge_request, :with_accessibility_reports) }
1996 1997 1998 1999 2000

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have accessibility reports' do
2001
      let(:merge_request) { create(:merge_request) }
2002 2003 2004 2005 2006

      it { is_expected.to be_falsey }
    end
  end

2007 2008 2009 2010
  describe '#has_coverage_reports?' do
    subject { merge_request.has_coverage_reports? }

    context 'when head pipeline has coverage reports' do
2011
      let(:merge_request) { create(:merge_request, :with_coverage_reports) }
2012 2013 2014 2015 2016

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have coverage reports' do
2017
      let(:merge_request) { create(:merge_request) }
2018 2019 2020 2021 2022

      it { is_expected.to be_falsey }
    end
  end

2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046
  describe '#has_codequality_mr_diff_report?' do
    subject { merge_request.has_codequality_mr_diff_report? }

    context 'when head pipeline has codequality mr diff report' do
      let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports) }

      it { is_expected.to be_truthy }

      context 'when feature flag is disabled' do
        before do
          stub_feature_flags(codequality_mr_diff: false)
        end

        it { is_expected.to be_falsey }
      end
    end

    context 'when head pipeline does not have codeqquality mr diff report' do
      let(:merge_request) { create(:merge_request) }

      it { is_expected.to be_falsey }
    end
  end

2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064
  describe '#has_codequality_reports?' do
    subject { merge_request.has_codequality_reports? }

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

    context 'when head pipeline has a codequality report' do
      let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have a codequality report' do
      let(:merge_request) { create(:merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

2065 2066 2067
  describe '#has_terraform_reports?' do
    context 'when head pipeline has terraform reports' do
      it 'returns true' do
2068
        merge_request = create(:merge_request, :with_terraform_reports)
2069 2070 2071 2072 2073 2074 2075

        expect(merge_request.has_terraform_reports?).to be_truthy
      end
    end

    context 'when head pipeline does not have terraform reports' do
      it 'returns false' do
2076
        merge_request = create(:merge_request)
2077 2078 2079 2080 2081 2082

        expect(merge_request.has_terraform_reports?).to be_falsey
      end
    end
  end

2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126
  describe '#has_sast_reports?' do
    subject { merge_request.has_sast_reports? }

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

    before do
      stub_licensed_features(sast: true)
    end

    context 'when head pipeline has sast reports' do
      let(:merge_request) { create(:merge_request, :with_sast_reports, source_project: project) }

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have sast reports' do
      let(:merge_request) { create(:merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

  describe '#has_secret_detection_reports?' do
    subject { merge_request.has_secret_detection_reports? }

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

    before do
      stub_licensed_features(secret_detection: true)
    end

    context 'when head pipeline has secret detection reports' do
      let(:merge_request) { create(:merge_request, :with_secret_detection_reports, source_project: project) }

      it { is_expected.to be_truthy }
    end

    context 'when head pipeline does not have secrets detection reports' do
      let(:merge_request) { create(:merge_request, source_project: project) }

      it { is_expected.to be_falsey }
    end
  end

2127
  describe '#calculate_reactive_cache' do
2128
    let(:merge_request) { create(:merge_request) }
2129

2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145
    subject { merge_request.calculate_reactive_cache(service_class_name) }

    context 'when given an unknown service class name' do
      let(:service_class_name) { 'Integer' }

      it 'raises a NameError exception' do
        expect { subject }.to raise_error(NameError, service_class_name)
      end
    end

    context 'when given a known service class name' do
      let(:service_class_name) { 'Ci::CompareTestReportsService' }

      it 'does not raises a NameError exception' do
        allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)

2146
        expect { subject }.not_to raise_error
2147 2148 2149 2150
      end
    end
  end

2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199
  describe '#find_exposed_artifacts' do
    let(:project) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
    let(:pipeline) { merge_request.head_pipeline }

    subject { merge_request.find_exposed_artifacts }

    context 'when head pipeline has exposed artifacts' do
      let!(:job) do
        create(:ci_build, options: { artifacts: { expose_as: 'artifact', paths: ['ci_artifacts.txt'] } }, pipeline: pipeline)
      end

      let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }

      context 'when reactive cache worker is parsing results asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect(subject[:status]).to eq(:parsed)
        end

        context 'when an error occurrs' do
          before do
            expect_next_instance_of(Ci::FindExposedArtifactsService) do |service|
              expect(service).to receive(:for_pipeline)
                .and_raise(StandardError.new)
            end
          end

          it 'returns an error message' do
            expect(subject[:status]).to eq(:error)
          end
        end

        context 'when cached results is not latest' do
          before do
            allow_next_instance_of(Ci::GenerateExposedArtifactsReportService) do |service|
              allow(service).to receive(:latest?).and_return(false)
            end
          end

2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247
          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#find_coverage_reports' do
    let(:project) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) }
    let(:pipeline) { merge_request.head_pipeline }

    subject { merge_request.find_coverage_reports }

    context 'when head pipeline has coverage reports' do
      context 'when reactive cache worker is parsing results asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect(subject[:status]).to eq(:parsed)
        end

        context 'when an error occurrs' do
          before do
            merge_request.update!(head_pipeline: nil)
          end

          it 'returns an error message' do
            expect(subject[:status]).to eq(:error)
          end
        end

        context 'when cached results is not latest' do
          before do
            allow_next_instance_of(Ci::GenerateCoverageReportsService) do |service|
              allow(service).to receive(:latest?).and_return(false)
            end
          end

2248 2249 2250 2251 2252 2253 2254 2255
          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303
  describe '#find_codequality_mr_diff_reports' do
    let(:project) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) }
    let(:pipeline) { merge_request.head_pipeline }

    subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports }

    context 'when head pipeline has coverage reports' do
      context 'when reactive cache worker is parsing results asynchronously' do
        it 'returns status' do
          expect(mr_diff_report[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
          expect(mr_diff_report[:status]).to eq(:parsed)
        end

        context 'when an error occurrs' do
          before do
            merge_request.update!(head_pipeline: nil)
          end

          it 'returns an error message' do
            expect(mr_diff_report[:status]).to eq(:error)
          end
        end

        context 'when cached results is not latest' do
          before do
            allow_next_instance_of(Ci::GenerateCodequalityMrDiffReportService) do |service|
              allow(service).to receive(:latest?).and_return(false)
            end
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { mr_diff_report }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

2304 2305 2306 2307 2308 2309 2310
  describe '#compare_test_reports' do
    subject { merge_request.compare_test_reports }

    let(:project) { create(:project, :repository) }
    let(:merge_request) { create(:merge_request, source_project: project) }

    let!(:base_pipeline) do
2311 2312 2313 2314 2315
      create(:ci_pipeline,
             :with_test_reports,
             project: project,
             ref: merge_request.target_branch,
             sha: merge_request.diff_base_sha)
2316 2317
    end

2318 2319
    before do
      merge_request.update!(head_pipeline_id: head_pipeline.id)
2320 2321 2322
    end

    context 'when head pipeline has test reports' do
2323 2324 2325 2326 2327 2328
      let!(:head_pipeline) do
        create(:ci_pipeline,
               :with_test_reports,
               project: project,
               ref: merge_request.source_branch,
               sha: merge_request.diff_head_sha)
2329
      end
2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342

      context 'when reactive cache worker is parsing asynchronously' do
        it 'returns status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns status and data' do
2343
          expect_any_instance_of(Ci::CompareTestReportsService)
2344
            .to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
2345

2346
          subject
2347
        end
2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358

        context 'when cached results is not latest' do
          before do
            allow_any_instance_of(Ci::CompareTestReportsService)
              .to receive(:latest?).and_return(false)
          end

          it 'raises and InvalidateReactiveCache error' do
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
2359 2360 2361 2362
      end
    end

    context 'when head pipeline does not have test reports' do
2363 2364 2365 2366 2367 2368 2369
      let!(:head_pipeline) do
        create(:ci_pipeline,
               project: project,
               ref: merge_request.source_branch,
               sha: merge_request.diff_head_sha)
      end

2370 2371
      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
2372
        expect(subject[:status_reason]).to eq('This merge request does not have test reports')
2373 2374 2375 2376
      end
    end
  end

2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425
  describe '#compare_accessibility_reports' do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:merge_request, reload: true) { create(:merge_request, :with_accessibility_reports, source_project: project) }
    let_it_be(:pipeline) { merge_request.head_pipeline }

    subject { merge_request.compare_accessibility_reports }

    context 'when head pipeline has accessibility reports' do
      let(:job) do
        create(:ci_build, options: { artifacts: { reports: { pa11y: ['accessibility.json'] } } }, pipeline: pipeline)
      end

      let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }

      context 'when reactive cache worker is parsing results asynchronously' do
        it 'returns parsing status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns parsed status' do
          expect(subject[:status]).to eq(:parsed)
          expect(subject[:data]).to be_present
        end

        context 'when an error occurrs' do
          before do
            merge_request.update!(head_pipeline: nil)
          end

          it 'returns an error status' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:status_reason]).to eq("This merge request does not have accessibility reports")
          end
        end

        context 'when cached result is not latest' do
          before do
            allow_next_instance_of(Ci::CompareAccessibilityReportsService) do |service|
              allow(service).to receive(:latest?).and_return(false)
            end
          end

          it 'raises an InvalidateReactiveCache error' do
2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

  describe '#compare_codequality_reports' do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:merge_request, reload: true) { create(:merge_request, :with_codequality_reports, source_project: project) }
    let_it_be(:pipeline) { merge_request.head_pipeline }

    subject { merge_request.compare_codequality_reports }

    context 'when head pipeline has codequality report' do
      let(:job) do
        create(:ci_build, options: { artifacts: { reports: { codeclimate: ['codequality.json'] } } }, pipeline: pipeline)
      end

      let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }

      context 'when reactive cache worker is parsing results asynchronously' do
        it 'returns parsing status' do
          expect(subject[:status]).to eq(:parsing)
        end
      end

      context 'when reactive cache worker is inline' do
        before do
          synchronous_reactive_cache(merge_request)
        end

        it 'returns parsed status' do
          expect(subject[:status]).to eq(:parsed)
          expect(subject[:data]).to be_present
        end

        context 'when an error occurrs' do
          before do
            merge_request.update!(head_pipeline: nil)
          end

          it 'returns an error status' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:status_reason]).to eq("This merge request does not have codequality reports")
          end
        end

        context 'when cached result is not latest' do
          before do
            allow_next_instance_of(Ci::CompareCodequalityReportsService) do |service|
              allow(service).to receive(:latest?).and_return(false)
            end
          end

          it 'raises an InvalidateReactiveCache error' do
2482 2483 2484 2485 2486 2487 2488
            expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
          end
        end
      end
    end
  end

2489
  describe '#all_commit_shas' do
2490
    context 'when merge request is persisted' do
2491
      let(:all_commit_shas) do
2492 2493
        subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
      end
2494

2495
      shared_examples 'returning all SHA' do
2496
        it 'returns all SHAs from all merge_request_diffs' do
2497
          expect(subject.merge_request_diffs.size).to eq(2)
2498
          expect(subject.all_commit_shas).to match_array(all_commit_shas)
2499
        end
2500 2501
      end

2502 2503
      context 'with a completely different branch' do
        before do
2504
          subject.update(target_branch: 'csv')
2505 2506 2507
        end

        it_behaves_like 'returning all SHA'
2508 2509
      end

2510 2511
      context 'with a branch having no difference' do
        before do
2512
          subject.update(target_branch: 'branch-merged')
2513 2514 2515 2516 2517
          subject.reload # make sure commits were not cached
        end

        it_behaves_like 'returning all SHA'
      end
2518 2519
    end

2520
    context 'when merge request is not persisted' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
2521 2522
      let_it_be(:project) { create(:project, :repository) }

2523 2524 2525 2526
      context 'when compare commits are set in the service' do
        let(:commit) { spy('commit') }

        subject do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
2527
          build(:merge_request, source_project: project, compare_commits: [commit, commit])
2528 2529 2530
        end

        it 'returns commits from compare commits temporary data' do
2531
          expect(subject.all_commit_shas).to eq [commit, commit]
2532
        end
2533 2534
      end

2535
      context 'when compare commits are not set in the service' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
2536
        subject { build(:merge_request, source_project: project) }
2537 2538

        it 'returns array with diff head sha element only' do
2539
          expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
2540 2541
        end
      end
2542 2543 2544
    end
  end

2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560
  describe '#short_merge_commit_sha' do
    let(:merge_request) { build_stubbed(:merge_request) }

    it 'returns short id when there is a merge_commit_sha' do
      merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d'

      expect(merge_request.short_merge_commit_sha).to eq('f7ce827c')
    end

    it 'returns nil when there is no merge_commit_sha' do
      merge_request.merge_commit_sha = nil

      expect(merge_request.short_merge_commit_sha).to be_nil
    end
  end

2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614
  describe '#merged_commit_sha' do
    it 'returns nil when not merged' do
      expect(subject.merged_commit_sha).to be_nil
    end

    context 'when the MR is merged' do
      let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' }

      before do
        subject.mark_as_merged!
      end

      it 'returns merge_commit_sha when there is a merge_commit_sha' do
        subject.update_attribute(:merge_commit_sha, sha)

        expect(subject.merged_commit_sha).to eq(sha)
      end

      it 'returns squash_commit_sha when there is a squash_commit_sha' do
        subject.update_attribute(:squash_commit_sha, sha)

        expect(subject.merged_commit_sha).to eq(sha)
      end

      it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do
        allow(subject).to receive(:diff_head_sha).and_return(sha)

        expect(subject.merged_commit_sha).to eq(sha)
      end
    end
  end

  describe '#short_merged_commit_sha' do
    context 'when merged_commit_sha is nil' do
      before do
        allow(subject).to receive(:merged_commit_sha).and_return(nil)
      end

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

    context 'when merged_commit_sha is present' do
      before do
        allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d')
      end

      it 'returns shortened merged_commit_sha' do
        expect(subject.short_merged_commit_sha).to eq('f7ce827c')
      end
    end
  end

2615
  describe '#can_be_reverted?' do
2616 2617
    subject { create(:merge_request, source_project: create(:project, :repository)) }

2618 2619
    context 'when there is no merge_commit for the MR' do
      before do
2620
        subject.metrics.update!(merged_at: Time.current.utc)
2621 2622 2623 2624 2625 2626 2627 2628 2629 2630
      end

      it 'returns false' do
        expect(subject.can_be_reverted?(nil)).to be_falsey
      end
    end

    context 'when the MR has been merged' do
      before do
        MergeRequests::MergeService
2631
          .new(subject.target_project, subject.author, { sha: subject.diff_head_sha })
2632 2633 2634 2635 2636 2637 2638 2639 2640
          .execute(subject)
      end

      context 'when there is no revert commit' do
        it 'returns true' do
          expect(subject.can_be_reverted?(nil)).to be_truthy
        end
      end

2641 2642 2643 2644 2645 2646 2647 2648 2649 2650
      context 'when there is no merged_at for the MR' do
        before do
          subject.metrics.update!(merged_at: nil)
        end

        it 'returns true' do
          expect(subject.can_be_reverted?(nil)).to be_truthy
        end
      end

2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666
      context 'when there is a revert commit' do
        let(:current_user) { subject.author }
        let(:branch) { subject.target_branch }
        let(:project) { subject.target_project }

        let(:revert_commit_id) do
          params = {
            commit: subject.merge_commit,
            branch_name: branch,
            start_branch: branch
          }

          Commits::RevertService.new(project, current_user, params).execute[:result]
        end

        before do
2667
          project.add_maintainer(current_user)
2668 2669 2670 2671 2672 2673 2674

          ProcessCommitWorker.new.perform(project.id,
                                          current_user.id,
                                          project.commit(revert_commit_id).to_hash,
                                          project.default_branch == branch)
        end

2675 2676 2677 2678 2679 2680 2681 2682 2683 2684
        context 'but merged at timestamp cannot be found' do
          before do
            allow(subject).to receive(:merged_at) { nil }
          end

          it 'returns false' do
            expect(subject.can_be_reverted?(current_user)).to be_falsey
          end
        end

2685 2686 2687 2688 2689 2690
        context 'when the revert commit is mentioned in a note after the MR was merged' do
          it 'returns false' do
            expect(subject.can_be_reverted?(current_user)).to be_falsey
          end
        end

2691 2692 2693 2694 2695 2696 2697 2698 2699 2700
        context 'when there is no merged_at for the MR' do
          before do
            subject.metrics.update!(merged_at: nil)
          end

          it 'returns false' do
            expect(subject.can_be_reverted?(current_user)).to be_falsey
          end
        end

2701
        context 'when the revert commit is mentioned in a note just before the MR was merged' do
2702
          before do
2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713
            subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
          end

          it 'returns false' do
            expect(subject.can_be_reverted?(current_user)).to be_falsey
          end
        end

        context 'when the revert commit is mentioned in a note long before the MR was merged' do
          before do
            subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
2714 2715 2716 2717 2718 2719 2720 2721 2722 2723
          end

          it 'returns true' do
            expect(subject.can_be_reverted?(current_user)).to be_truthy
          end
        end
      end
    end
  end

2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758
  describe '#merged_at' do
    context 'when MR is not merged' do
      let(:merge_request) { create(:merge_request, :closed) }

      it 'returns nil' do
        expect(merge_request.merged_at).to be_nil
      end
    end

    context 'when metrics has merged_at data' do
      let(:merge_request) { create(:merge_request, :merged) }

      before do
        merge_request.metrics.update!(merged_at: 1.day.ago)
      end

      it 'returns metrics merged_at' do
        expect(merge_request.merged_at).to eq(merge_request.metrics.merged_at)
      end
    end

    context 'when merged event is persisted, but no metrics merged_at is persisted' do
      let(:user) { create(:user) }
      let(:merge_request) { create(:merge_request, :merged) }

      before do
        EventCreateService.new.merge_mr(merge_request, user)
      end

      it 'returns merged event creation date' do
        expect(merge_request.merge_event).to be_persisted
        expect(merge_request.merged_at).to eq(merge_request.merge_event.created_at)
      end
    end

2759 2760 2761 2762
    context 'when no metrics or merge event exists' do
      let(:user) { create(:user) }
      let(:merge_request) { create(:merge_request, :merged) }

2763
      before do
2764
        merge_request.metrics.destroy!
2765 2766
      end

2767
      context 'when resource event for the merge exists' do
2768 2769 2770 2771 2772 2773 2774
        before do
          SystemNoteService.change_status(merge_request,
                                          merge_request.target_project,
                                          user,
                                          merge_request.state, nil)
        end

2775
        it 'returns the resource event creation date' do
2776 2777
          expect(merge_request.reload.metrics).to be_nil
          expect(merge_request.merge_event).to be_nil
2778 2779
          expect(merge_request.resource_state_events.count).to eq(1)
          expect(merge_request.merged_at).to eq(merge_request.resource_state_events.first.created_at)
2780 2781
        end
      end
2782

2783 2784 2785 2786 2787 2788
      context 'when system note for the merge exists' do
        before do
          # We do not create these system notes anymore but we need this to work for existing MRs
          # that used system notes instead of resource state events
          create(:note, :system, noteable: merge_request, note: 'merged')
        end
2789

2790 2791 2792 2793 2794 2795
        it 'returns the merging note creation date' do
          expect(merge_request.reload.metrics).to be_nil
          expect(merge_request.merge_event).to be_nil
          expect(merge_request.notes.count).to eq(1)
          expect(merge_request.merged_at).to eq(merge_request.notes.first.created_at)
        end
2796 2797 2798 2799
      end
    end
  end

Yorick Peterse's avatar
Yorick Peterse committed
2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820
  describe '#participants' do
    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
2821 2822 2823 2824 2825

  describe 'cached counts' do
    it 'updates when assignees change' do
      user1 = create(:user)
      user2 = create(:user)
2826
      mr = create(:merge_request, assignees: [user1])
2827 2828
      mr.project.add_developer(user1)
      mr.project.add_developer(user2)
2829

2830 2831
      expect(user1.assigned_open_merge_requests_count).to eq(1)
      expect(user2.assigned_open_merge_requests_count).to eq(0)
2832

2833
      mr.assignees = [user2]
2834

2835 2836
      expect(user1.assigned_open_merge_requests_count).to eq(0)
      expect(user2.assigned_open_merge_requests_count).to eq(1)
2837 2838
    end
  end
2839

2840
  describe '#merge_async' do
2841 2842 2843
    it 'enqueues MergeWorker job and updates merge_jid' do
      merge_request = create(:merge_request)
      user_id = double(:user_id)
2844
      params = {}
2845 2846
      merge_jid = 'hash-123'

2847
      expect(merge_request).to receive(:expire_etag_cache)
2848 2849 2850 2851
      expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
        merge_jid
      end

2852
      merge_request.merge_async(user_id, params)
2853 2854 2855 2856 2857

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

2858 2859 2860 2861 2862 2863 2864 2865 2866 2867
  describe '#rebase_async' do
    let(:merge_request) { create(:merge_request) }
    let(:user_id) { double(:user_id) }
    let(:rebase_jid) { 'rebase-jid' }

    subject(:execute) { merge_request.rebase_async(user_id) }

    it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
      expect(RebaseWorker)
        .to receive(:perform_async)
2868
        .with(merge_request.id, user_id, false)
2869 2870
        .and_return(rebase_jid)

2871
      expect(merge_request).to receive(:expire_etag_cache)
2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891
      expect(merge_request).to receive(:lock!).and_call_original

      execute

      expect(merge_request.rebase_jid).to eq(rebase_jid)
    end

    it 'refuses to enqueue a job if a rebase is in progress' do
      merge_request.update_column(:rebase_jid, rebase_jid)

      expect(RebaseWorker).not_to receive(:perform_async)
      expect(Gitlab::SidekiqStatus)
        .to receive(:running?)
        .with(rebase_jid)
        .and_return(true)

      expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
    end

    it 'refuses to enqueue a job if the MR is not open' do
2892
      merge_request.update_column(:state_id, 5)
2893 2894 2895 2896 2897

      expect(RebaseWorker).not_to receive(:perform_async)

      expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
    end
2898 2899 2900 2901 2902 2903 2904

    it "raises ActiveRecord::LockWaitTimeout after 6 tries" do
      expect(merge_request).to receive(:with_lock).exactly(6).times.and_raise(ActiveRecord::LockWaitTimeout)
      expect(RebaseWorker).not_to receive(:perform_async)

      expect { execute }.to raise_error(MergeRequest::RebaseLockTimeout)
    end
2905 2906
  end

2907
  describe '#mergeable?' do
2908
    subject { build_stubbed(:merge_request) }
2909

2910 2911
    it 'returns false if #mergeable_state? is false' do
      expect(subject).to receive(:mergeable_state?) { false }
2912

2913
      expect(subject.mergeable?).to be_falsey
2914 2915
    end

2916
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
2917
      allow(subject).to receive(:mergeable_state?) { true }
2918
      expect(subject).to receive(:check_mergeability)
2919
      expect(subject).to receive(:can_be_merged?) { true }
2920 2921 2922

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

Patrick Bajao's avatar
Patrick Bajao committed
2924 2925 2926 2927 2928 2929 2930 2931
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is false' do
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_mergeability)
      expect(subject).to receive(:can_be_merged?) { false }

      expect(subject.mergeable?).to be_falsey
    end

2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977
    context 'with skip_ci_check option' do
      before do
        allow(subject).to receive_messages(check_mergeability: nil,
                                           can_be_merged?: true,
                                           broken?: false)
      end

      where(:mergeable_ci_state, :skip_ci_check, :expected_mergeable) do
        false | false | false
        false | true  | true
        true  | false | true
        true  | true  | true
      end

      with_them do
        it 'overrides mergeable_ci_state?' do
          allow(subject).to receive(:mergeable_ci_state?) { mergeable_ci_state }

          expect(subject.mergeable?(skip_ci_check: skip_ci_check)).to eq(expected_mergeable)
        end
      end
    end

    context 'with skip_discussions_check option' do
      before do
        allow(subject).to receive_messages(mergeable_ci_state?: true,
                                           check_mergeability: nil,
                                           can_be_merged?: true,
                                           broken?: false)
      end

      where(:mergeable_discussions_state, :skip_discussions_check, :expected_mergeable) do
        false | false | false
        false | true  | true
        true  | false | true
        true  | true  | true
      end

      with_them do
        it 'overrides mergeable_discussions_state?' do
          allow(subject).to receive(:mergeable_discussions_state?) { mergeable_discussions_state }

          expect(subject.mergeable?(skip_discussions_check: skip_discussions_check)).to eq(expected_mergeable)
        end
      end
    end
2978 2979
  end

2980 2981 2982
  describe '#check_mergeability' do
    let(:mergeability_service) { double }

2983 2984
    subject { create(:merge_request, merge_status: 'unchecked') }

2985 2986 2987 2988 2989 2990
    before do
      allow(MergeRequests::MergeabilityCheckService).to receive(:new) do
        mergeability_service
      end
    end

2991
    shared_examples_for 'method that executes MergeabilityCheckService' do
2992 2993 2994 2995 2996
      it 'executes MergeabilityCheckService' do
        expect(mergeability_service).to receive(:execute)

        subject.check_mergeability
      end
2997 2998

      context 'when async is true' do
2999 3000
        it 'executes MergeabilityCheckService asynchronously' do
          expect(mergeability_service).to receive(:async_execute)
3001

3002
          subject.check_mergeability(async: true)
3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016
        end
      end
    end

    context 'if the merge status is unchecked' do
      it_behaves_like 'method that executes MergeabilityCheckService'
    end

    context 'if the merge status is checking' do
      before do
        subject.mark_as_checking!
      end

      it_behaves_like 'method that executes MergeabilityCheckService'
3017 3018 3019
    end

    context 'if the merge status is checked' do
3020 3021 3022 3023
      before do
        subject.mark_as_mergeable!
      end

3024 3025
      it 'does not call MergeabilityCheckService' do
        expect(MergeRequests::MergeabilityCheckService).not_to receive(:new)
3026

3027
        subject.check_mergeability
3028 3029 3030 3031
      end
    end
  end

3032
  describe '#mergeable_state?' do
3033
    subject { create(:merge_request) }
3034

3035
    it 'checks if merge request can be merged' do
3036
      allow(subject).to receive(:mergeable_ci_state?) { true }
3037
      expect(subject).to receive(:check_mergeability)
3038 3039 3040 3041 3042

      subject.mergeable?
    end

    context 'when not open' do
3043 3044 3045
      before do
        subject.close
      end
3046 3047

      it 'returns false' do
3048
        expect(subject.mergeable_state?).to be_falsey
3049 3050 3051 3052
      end
    end

    context 'when working in progress' do
3053
      before do
3054
        subject.title = '[Draft] MR'
3055
      end
3056 3057

      it 'returns false' do
3058
        expect(subject.mergeable_state?).to be_falsey
3059 3060 3061 3062
      end
    end

    context 'when broken' do
3063 3064 3065
      before do
        allow(subject).to receive(:broken?) { true }
      end
3066 3067

      it 'returns false' do
3068
        expect(subject.mergeable_state?).to be_falsey
3069 3070 3071 3072
      end
    end

    context 'when failed' do
3073
      context 'when #mergeable_ci_state? is false' do
3074
        before do
3075
          allow(subject).to receive(:mergeable_ci_state?) { false }
3076 3077 3078 3079
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
3080
        end
3081 3082 3083 3084

        it 'returns true when skipping ci check' do
          expect(subject.mergeable_state?(skip_ci_check: true)).to be(true)
        end
3085
      end
3086

3087
      context 'when #mergeable_discussions_state? is false' do
3088 3089 3090 3091 3092 3093 3094
        before do
          allow(subject).to receive(:mergeable_discussions_state?) { false }
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
        end
3095 3096 3097 3098

        it 'returns true when skipping discussions check' do
          expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true)
        end
3099
      end
3100 3101 3102
    end
  end

3103 3104 3105 3106 3107 3108
  describe "#public_merge_status" do
    using RSpec::Parameterized::TableSyntax
    subject { build(:merge_request, merge_status: status) }

    where(:status, :public_status) do
      'cannot_be_merged_rechecking' | 'checking'
Patrick Bajao's avatar
Patrick Bajao committed
3109
      'preparing'                   | 'checking'
3110 3111 3112 3113 3114 3115 3116 3117 3118
      'checking'                    | 'checking'
      'cannot_be_merged'            | 'cannot_be_merged'
    end

    with_them do
      it { expect(subject.public_merge_status).to eq(public_status) }
    end
  end

3119
  describe "#head_pipeline_active? " do
3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142
    context 'when project lacks a head_pipeline relation' do
      before do
        subject.head_pipeline = nil
      end

      it 'returns false' do
        expect(subject.head_pipeline_active?).to be false
      end
    end

    context 'when project has a head_pipeline relation' do
      let(:pipeline) { create(:ci_empty_pipeline) }

      before do
        allow(subject).to receive(:head_pipeline) { pipeline }
      end

      it 'accesses the value from the head_pipeline' do
        expect(subject.head_pipeline)
          .to receive(:active?)

        subject.head_pipeline_active?
      end
3143 3144 3145 3146
    end
  end

  describe "#actual_head_pipeline_success? " do
3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169
    context 'when project lacks an actual_head_pipeline relation' do
      before do
        allow(subject).to receive(:actual_head_pipeline) { nil }
      end

      it 'returns false' do
        expect(subject.actual_head_pipeline_success?).to be false
      end
    end

    context 'when project has a actual_head_pipeline relation' do
      let(:pipeline) { create(:ci_empty_pipeline) }

      before do
        allow(subject).to receive(:actual_head_pipeline) { pipeline }
      end

      it 'accesses the value from the actual_head_pipeline' do
        expect(subject.actual_head_pipeline)
          .to receive(:success?)

        subject.actual_head_pipeline_success?
      end
3170 3171 3172
    end
  end

3173
  describe "#actual_head_pipeline_active? " do
3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196
    context 'when project lacks an actual_head_pipeline relation' do
      before do
        allow(subject).to receive(:actual_head_pipeline) { nil }
      end

      it 'returns false' do
        expect(subject.actual_head_pipeline_active?).to be false
      end
    end

    context 'when project has a actual_head_pipeline relation' do
      let(:pipeline) { create(:ci_empty_pipeline) }

      before do
        allow(subject).to receive(:actual_head_pipeline) { pipeline }
      end

      it 'accesses the value from the actual_head_pipeline' do
        expect(subject.actual_head_pipeline)
          .to receive(:active?)

        subject.actual_head_pipeline_active?
      end
3197 3198 3199
    end
  end

3200
  describe '#mergeable_ci_state?' do
3201
    let(:pipeline) { create(:ci_empty_pipeline) }
3202

3203
    context 'when it is only allowed to merge when build is green' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3204
      let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
3205

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3206
      subject { build(:merge_request, source_project: project) }
3207

3208
      context 'and a failed pipeline is associated' do
3209
        before do
3210
          pipeline.update(status: 'failed', sha: subject.diff_head_sha)
3211
          allow(subject).to receive(:head_pipeline) { pipeline }
3212
        end
3213

3214
        it { expect(subject.mergeable_ci_state?).to be_falsey }
3215 3216
      end

3217 3218
      context 'and a successful pipeline is associated' do
        before do
3219
          pipeline.update(status: 'success', sha: subject.diff_head_sha)
3220
          allow(subject).to receive(:head_pipeline) { pipeline }
3221 3222 3223 3224 3225 3226 3227
        end

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

      context 'and a skipped pipeline is associated' do
        before do
3228
          pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
3229
          allow(subject).to receive(:head_pipeline).and_return(pipeline)
3230 3231
        end

3232
        it { expect(subject.mergeable_ci_state?).to be_falsey }
3233 3234
      end

3235
      context 'when no pipeline is associated' do
3236
        before do
3237 3238 3239 3240 3241 3242 3243 3244
          allow(subject).to receive(:head_pipeline).and_return(nil)
        end

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

    context 'when it is only allowed to merge when build is green or skipped' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3245
      let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true, allow_merge_on_skipped_pipeline: true) }
3246

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3247
      subject { build(:merge_request, source_project: project) }
3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278

      context 'and a failed pipeline is associated' do
        before do
          pipeline.update!(status: 'failed', sha: subject.diff_head_sha)
          allow(subject).to receive(:head_pipeline).and_return(pipeline)
        end

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

      context 'and a successful pipeline is associated' do
        before do
          pipeline.update!(status: 'success', sha: subject.diff_head_sha)
          allow(subject).to receive(:head_pipeline).and_return(pipeline)
        end

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

      context 'and a skipped pipeline is associated' do
        before do
          pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
          allow(subject).to receive(:head_pipeline).and_return(pipeline)
        end

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

      context 'when no pipeline is associated' do
        before do
          allow(subject).to receive(:head_pipeline).and_return(nil)
3279 3280
        end

3281
        it { expect(subject.mergeable_ci_state?).to be_falsey }
3282 3283 3284
      end
    end

3285
    context 'when merges are not restricted to green builds' do
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3286
      let_it_be(:project) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: false) }
3287

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
3288
      subject { build(:merge_request, source_project: project) }
3289

3290
      context 'and a failed pipeline is associated' do
3291
        before do
3292
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
3293
          allow(subject).to receive(:head_pipeline) { pipeline }
3294 3295 3296 3297 3298
        end

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

3299
      context 'when no pipeline is associated' do
3300
        before do
3301
          allow(subject).to receive(:head_pipeline) { nil }
3302 3303 3304
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
3305
      end
3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322

      context 'and a skipped pipeline is associated' do
        before do
          pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
          allow(subject).to receive(:head_pipeline).and_return(pipeline)
        end

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

      context 'when no pipeline is associated' do
        before do
          allow(subject).to receive(:head_pipeline).and_return(nil)
        end

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

3326
  describe '#mergeable_discussions_state?' do
3327
    let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
3328

3329
    context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
3330
      let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
3331

3332
      context 'with all discussions resolved' do
3333
        before do
3334
          merge_request.discussions.each { |d| d.resolve!(merge_request.author) }
3335 3336 3337
        end

        it 'returns true' do
3338
          expect(merge_request.mergeable_discussions_state?).to be_truthy
3339 3340 3341
        end
      end

3342
      context 'with unresolved discussions' do
3343
        before do
3344
          merge_request.discussions.each(&:unresolve!)
3345 3346 3347
        end

        it 'returns false' do
3348
          expect(merge_request.mergeable_discussions_state?).to be_falsey
3349 3350
        end
      end
3351 3352 3353

      context 'with no discussions' do
        before do
Doug Stull's avatar
Doug Stull committed
3354
          merge_request.notes.destroy_all # rubocop: disable Cop/DestroyAll
3355 3356 3357 3358 3359 3360
        end

        it 'returns true' do
          expect(merge_request.mergeable_discussions_state?).to be_truthy
        end
      end
3361 3362
    end

3363
    context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
3364
      let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
3365

3366
      context 'with unresolved discussions' do
3367
        before do
3368
          merge_request.discussions.each(&:unresolve!)
3369 3370 3371
        end

        it 'returns true' do
3372
          expect(merge_request.mergeable_discussions_state?).to be_truthy
3373 3374 3375 3376 3377
        end
      end
    end
  end

Douwe Maan's avatar
Douwe Maan committed
3378
  describe "#environments_for" do
3379
    let(:project)       { create(:project, :repository) }
Douwe Maan's avatar
Douwe Maan committed
3380
    let(:user)          { project.creator }
Z.J. van de Weg's avatar
Z.J. van de Weg committed
3381
    let(:merge_request) { create(:merge_request, source_project: project) }
3382 3383 3384 3385
    let(:source_branch) { merge_request.source_branch }
    let(:target_branch) { merge_request.target_branch }
    let(:source_oid) { project.commit(source_branch).id }
    let(:target_oid) { project.commit(target_branch).id }
Z.J. van de Weg's avatar
Z.J. van de Weg committed
3386

Douwe Maan's avatar
Douwe Maan committed
3387
    before do
3388 3389
      merge_request.source_project.add_maintainer(user)
      merge_request.target_project.add_maintainer(user)
Douwe Maan's avatar
Douwe Maan committed
3390 3391
    end

3392 3393
    context 'with multiple environments' do
      let(:environments) { create_list(:environment, 3, project: project) }
3394

3395
      before do
3396 3397
        create(:deployment, :success, environment: environments.first, ref: source_branch, sha: source_oid)
        create(:deployment, :success, environment: environments.second, ref: target_branch, sha: target_oid)
3398 3399 3400
      end

      it 'selects deployed environments' do
Douwe Maan's avatar
Douwe Maan committed
3401
        expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
3402
      end
3403 3404 3405 3406 3407 3408 3409 3410

      it 'selects latest deployed environment' do
        latest_environment = create(:environment, project: project)
        create(:deployment, :success, environment: latest_environment, ref: source_branch, sha: source_oid)

        expect(merge_request.environments_for(user)).to eq([environments.first, latest_environment])
        expect(merge_request.environments_for(user, latest: true)).to contain_exactly(latest_environment)
      end
3411 3412 3413
    end

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

3416 3417 3418 3419 3420 3421 3422 3423 3424
      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
3425
        create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
3426 3427
      end

3428
      it 'selects deployed environments', :sidekiq_might_not_need_inline do
Douwe Maan's avatar
Douwe Maan committed
3429
        expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
3430 3431 3432 3433 3434 3435
      end

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

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

3439
        it 'selects deployed environments', :sidekiq_might_not_need_inline do
Douwe Maan's avatar
Douwe Maan committed
3440
          expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
3441 3442
        end
      end
3443 3444 3445 3446 3447 3448 3449 3450
    end

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

      it 'returns an empty array' do
Douwe Maan's avatar
Douwe Maan committed
3451
        expect(merge_request.environments_for(user)).to be_empty
3452
      end
Z.J. van de Weg's avatar
Z.J. van de Weg committed
3453 3454 3455
    end
  end

3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469
  describe "#environments" do
    subject { merge_request.environments }

    let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
    let(:project) { merge_request.project }

    let(:pipeline) do
      create(:ci_pipeline,
        source: :merge_request_event,
        merge_request: merge_request, project: project,
        sha: merge_request.diff_head_sha,
        merge_requests_as_head_pipeline: [merge_request])
    end

3470
    let!(:job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) }
3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499

    it 'returns environments' do
      is_expected.to eq(pipeline.environments)
      expect(subject.count).to be(1)
    end

    context 'when pipeline is not associated with environments' do
      let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }

      it 'returns empty array' do
        is_expected.to be_empty
      end
    end

    context 'when pipeline is not a pipeline for merge request' do
      let(:pipeline) do
        create(:ci_pipeline,
          project: project,
          ref: 'feature',
          sha: merge_request.diff_head_sha,
          merge_requests_as_head_pipeline: [merge_request])
      end

      it 'returns empty relation' do
        is_expected.to be_empty
      end
    end
  end

3500
  describe "#reload_diff" do
3501 3502 3503
    it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
      user = create(:user)
      service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
3504

3505 3506 3507
      expect(MergeRequests::ReloadDiffsService)
        .to receive(:new).with(subject, user)
        .and_return(service)
3508

3509
      subject.reload_diff(user)
3510

3511
      expect(service).to have_received(:execute)
3512
    end
3513 3514 3515 3516 3517 3518 3519 3520 3521 3522

    context 'when using the after_update hook to update' do
      context 'when the branches are updated' do
        it 'uses the new heads to generate the diff' do
          expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) }
            .to change { subject.merge_request_diff.start_commit_sha }
            .and change { subject.merge_request_diff.head_commit_sha }
        end
      end
    end
3523
  end
3524

3525 3526 3527 3528 3529 3530
  describe '#update_diff_discussion_positions' do
    let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
    let(:commit) { subject.project.commit(sample_commit.id) }
    let(:old_diff_refs) { subject.diff_refs }

    before do
3531
      # Update merge_request_diff so that #diff_refs will return commit.diff_refs
3532 3533 3534 3535 3536 3537
      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
        )
3538

3539
        subject.reload_merge_request_diff
3540
      end
3541
    end
3542

3543
    it "updates diff discussion positions" do
3544
      expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
3545
        subject.project,
3546
        subject.author,
3547 3548
        old_diff_refs: old_diff_refs,
        new_diff_refs: commit.diff_refs,
3549
        paths: discussion.position.paths
3550 3551
      ).and_call_original

3552
      expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
3553 3554
      expect_any_instance_of(DiffNote).to receive(:save).once

3555 3556 3557 3558 3559 3560
      subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
                                               new_diff_refs: commit.diff_refs,
                                               current_user: subject.author)
    end

    context 'when resolve_outdated_diff_discussions is set' do
3561 3562
      let(:project) { create(:project, :repository) }

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

3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578
      before do
        discussion

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

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

        subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
                                                 new_diff_refs: commit.diff_refs,
                                                 current_user: subject.author)
      end
3579 3580
    end
  end
3581 3582

  describe '#branch_merge_base_commit' do
3583 3584
    let(:project) { create(:project, :repository) }

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

3587 3588 3589 3590 3591 3592 3593
    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
3594
        subject.project.repository.rm_branch(subject.author, subject.target_branch)
3595
        subject.clear_memoized_shas
3596 3597 3598 3599
      end

      it 'returns nil' do
        expect(subject.branch_merge_base_commit).to be_nil
3600 3601 3602 3603
      end
    end
  end

3604
  describe "#diff_refs" do
3605
    context "with diffs" do
3606 3607
      let(:project) { create(:project, :repository) }

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

3610
      let(:expected_diff_refs) do
3611 3612 3613 3614 3615
        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
        )
3616
      end
3617 3618 3619 3620 3621 3622

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

        expect_any_instance_of(Repository).not_to receive(:commit)

3623
        subject.diff_refs
3624 3625 3626
      end

      it "returns expected diff_refs" do
3627
        expect(subject.diff_refs).to eq(expected_diff_refs)
3628
      end
3629 3630 3631 3632 3633 3634 3635 3636 3637 3638

      context 'when importing' do
        before do
          subject.importing = true
        end

        it "returns MR diff_refs" do
          expect(subject.diff_refs).to eq(expected_diff_refs)
        end
      end
3639 3640
    end
  end
3641

3642
  describe "#source_project_missing?" do
3643
    let(:project) { create(:project) }
3644
    let(:forked_project) { fork_project(project) }
3645
    let(:user) { create(:user) }
3646
    let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
3647

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3648
    context "when the fork exists" do
3649 3650
      let(:merge_request) do
        create(:merge_request,
3651
          source_project: forked_project,
3652 3653 3654
          target_project: project)
      end

3655
      it { expect(merge_request.source_project_missing?).to be_falsey }
3656 3657
    end

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

3661
      it { expect(merge_request.source_project_missing?).to be_falsey }
3662 3663
    end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3664
    context "when the fork does not exist" do
3665
      let!(:merge_request) do
3666
        create(:merge_request,
3667
          source_project: forked_project,
3668 3669 3670
          target_project: project)
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3671
      it "returns true" do
3672 3673 3674
        unlink_project.execute
        merge_request.reload

3675
        expect(merge_request.source_project_missing?).to be_truthy
3676 3677 3678 3679
      end
    end
  end

3680
  describe '#merge_ongoing?' do
3681
    it 'returns true when the merge request is locked' do
3682
      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:locked])
3683 3684 3685 3686

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

3687
    it 'returns true when merge_id, MR is not merged and it has no running job' do
3688
      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
3689
      allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
3690 3691 3692

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

    it 'returns false when merge_jid is nil' do
3695
      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: nil)
3696 3697 3698 3699 3700

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

    it 'returns false if MR is merged' do
3701
      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:merged], merge_jid: 'foo')
3702 3703 3704 3705 3706

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

    it 'returns false if there is no merge job running' do
3707
      merge_request = build_stubbed(:merge_request, state_id: described_class.available_states[:opened], merge_jid: 'foo')
3708
      allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
3709 3710 3711

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

3714
  describe "#closed_or_merged_without_fork?" do
3715
    let(:project) { create(:project) }
3716
    let(:forked_project) { fork_project(project) }
3717
    let(:user) { create(:user) }
3718
    let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
3719

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3720
    context "when the merge request is closed" do
3721 3722
      let(:closed_merge_request) do
        create(:closed_merge_request,
3723
          source_project: forked_project,
3724 3725 3726
          target_project: project)
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3727
      it "returns false if the fork exist" do
3728
        expect(closed_merge_request.closed_or_merged_without_fork?).to be_falsey
3729 3730
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3731
      it "returns true if the fork does not exist" do
3732 3733 3734
        unlink_project.execute
        closed_merge_request.reload

3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754
        expect(closed_merge_request.closed_or_merged_without_fork?).to be_truthy
      end
    end

    context "when the merge request was merged" do
      let(:merged_merge_request) do
        create(:merged_merge_request,
          source_project: forked_project,
          target_project: project)
      end

      it "returns false if the fork exist" do
        expect(merged_merge_request.closed_or_merged_without_fork?).to be_falsey
      end

      it "returns true if the fork does not exist" do
        unlink_project.execute
        merged_merge_request.reload

        expect(merged_merge_request.closed_or_merged_without_fork?).to be_truthy
3755 3756
      end
    end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3757

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3758
    context "when the merge request is open" do
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3759 3760
      let(:open_merge_request) do
        create(:merge_request,
3761
          source_project: forked_project,
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3762 3763 3764 3765
          target_project: project)
      end

      it "returns false" do
3766
        expect(open_merge_request.closed_or_merged_without_fork?).to be_falsey
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3767 3768
      end
    end
3769
  end
3770

3771
  describe '#reopenable?' do
3772 3773 3774
    context 'when the merge request is closed' do
      it 'returns true' do
        subject.close
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3775

3776
        expect(subject.reopenable?).to be_truthy
3777 3778 3779
      end

      context 'forked project' do
3780
        let(:project)      { create(:project, :public) }
3781
        let(:user)         { create(:user) }
3782
        let(:forked_project) { fork_project(project, user) }
3783 3784

        let!(:merge_request) do
3785
          create(:closed_merge_request,
3786
            source_project: forked_project,
3787 3788 3789 3790
            target_project: project)
        end

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

3793
          expect(merge_request.reload.reopenable?).to be_falsey
3794 3795 3796
        end

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

3799
          expect(merge_request.reload.reopenable?).to be_falsey
3800 3801
        end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3802
        it 'returns false if the merge request is merged' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
3803
          merge_request.update(state: 'merged')
3804

3805
          expect(merge_request.reload.reopenable?).to be_falsey
3806 3807
        end
      end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3808 3809
    end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3810
    context 'when the merge request is opened' do
3811
      it 'returns false' do
3812
        expect(subject.reopenable?).to be_falsey
3813
      end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
3814 3815
    end
  end
3816

3817
  describe '#pipeline_coverage_delta' do
3818
    let!(:merge_request) { create(:merge_request) }
3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872

    let!(:source_pipeline) do
      create(:ci_pipeline,
        project: project,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha
      )
    end

    let!(:target_pipeline) do
      create(:ci_pipeline,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    def create_build(pipeline, coverage, name)
      create(:ci_build, :success, pipeline: pipeline, coverage: coverage, name: name)
      merge_request.update_head_pipeline
    end

    context 'when both source and target branches have coverage information' do
      it 'returns the appropriate coverage delta' do
        create_build(source_pipeline, 60.2, 'test:1')
        create_build(target_pipeline, 50, 'test:2')

        expect(merge_request.pipeline_coverage_delta).to eq('10.20')
      end
    end

    context 'when target branch does not have coverage information' do
      it 'returns nil' do
        create_build(source_pipeline, 50, 'test:1')

        expect(merge_request.pipeline_coverage_delta).to be_nil
      end
    end

    context 'when source branch does not have coverage information' do
      it 'returns nil for coverage_delta' do
        create_build(target_pipeline, 50, 'test:1')

        expect(merge_request.pipeline_coverage_delta).to be_nil
      end
    end

    context 'neither source nor target branch has coverage information' do
      it 'returns nil for coverage_delta' do
        expect(merge_request.pipeline_coverage_delta).to be_nil
      end
    end
  end

3873 3874 3875 3876 3877 3878 3879 3880 3881
  describe '#use_merge_base_pipeline_for_comparison?' do
    let(:project) { create(:project, :public, :repository) }
    let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }

    subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) }

    context 'when service class is Ci::CompareCodequalityReportsService' do
      let(:service_class) { 'Ci::CompareCodequalityReportsService' }

3882
      it { is_expected.to be_truthy }
3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943
    end

    context 'when service class is different' do
      let(:service_class) { 'Ci::GenerateCoverageReportsService' }

      it { is_expected.to be_falsey }
    end
  end

  describe '#comparison_base_pipeline' do
    subject(:pipeline) { merge_request.comparison_base_pipeline(service_class) }

    let(:project) { create(:project, :public, :repository) }
    let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
    let!(:base_pipeline) do
      create(:ci_pipeline,
        :with_test_reports,
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      )
    end

    context 'when service class is Ci::CompareCodequalityReportsService' do
      let(:service_class) { 'Ci::CompareCodequalityReportsService' }

      context 'when merge request has a merge request pipeline' do
        let(:merge_request) do
          create(:merge_request, :with_merge_request_pipeline)
        end

        let(:merge_base_pipeline) do
          create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
        end

        before do
          merge_base_pipeline
          merge_request.update_head_pipeline
        end

        it 'returns the merge_base_pipeline' do
          expect(pipeline).to eq(merge_base_pipeline)
        end
      end

      context 'when merge does not have a merge request pipeline' do
        it 'returns the base_pipeline' do
          expect(pipeline).to eq(base_pipeline)
        end
      end
    end

    context 'when service_class is different' do
      let(:service_class) { 'Ci::GenerateCoverageReportsService' }

      it 'returns the base_pipeline' do
        expect(pipeline).to eq(base_pipeline)
      end
    end
  end

3944 3945 3946 3947 3948 3949 3950 3951 3952
  describe '#base_pipeline' do
    let(:pipeline_arguments) do
      {
        project: project,
        ref: merge_request.target_branch,
        sha: merge_request.diff_base_sha
      }
    end

3953
    let(:project) { create(:project, :public, :repository) }
3954 3955
    let(:merge_request) { create(:merge_request, source_project: project) }

3956 3957 3958
    let!(:first_pipeline) { create(:ci_pipeline, pipeline_arguments) }
    let!(:last_pipeline) { create(:ci_pipeline, pipeline_arguments) }
    let!(:last_pipeline_with_other_ref) { create(:ci_pipeline, pipeline_arguments.merge(ref: 'other')) }
3959

3960
    it 'returns latest pipeline for the target branch' do
3961 3962 3963 3964
      expect(merge_request.base_pipeline).to eq(last_pipeline)
    end
  end

3965 3966 3967 3968 3969
  describe '#merge_base_pipeline' do
    let(:merge_request) do
      create(:merge_request, :with_merge_request_pipeline)
    end

3970
    let(:merge_base_pipeline) do
3971 3972 3973
      create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha)
    end

3974 3975
    before do
      merge_base_pipeline
3976
      merge_request.update_head_pipeline
3977
    end
3978

3979 3980
    it 'returns a pipeline pointing to a commit on the target ref' do
      expect(merge_request.merge_base_pipeline).to eq(merge_base_pipeline)
3981 3982 3983
    end
  end

3984
  describe '#has_commits?' do
3985
    it 'returns true when merge request diff has commits' do
3986 3987
      allow(subject.merge_request_diff).to receive(:commits_count)
        .and_return(2)
3988 3989 3990

      expect(subject.has_commits?).to be_truthy
    end
3991 3992 3993 3994 3995 3996 3997 3998 3999

    context 'when commits_count is nil' do
      it 'returns false' do
        allow(subject.merge_request_diff).to receive(:commits_count)
        .and_return(nil)

        expect(subject.has_commits?).to be_falsey
      end
    end
4000 4001 4002 4003
  end

  describe '#has_no_commits?' do
    before do
4004 4005
      allow(subject.merge_request_diff).to receive(:commits_count)
        .and_return(0)
4006 4007 4008 4009 4010 4011
    end

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

  describe '#merge_request_diff_for' do
4014 4015 4016
    let(:project) { create(:project, :repository) }

    subject { create(:merge_request, importing: true, source_project: project) }
4017

4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032
    let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
    let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
    let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }

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

    context 'with a commit SHA' do
      it 'returns the diffs' do
        expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
      end
    end
4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043

    it 'runs a single query on the initial call, and none afterwards' do
      expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
        .not_to exceed_query_limit(1)

      expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
        .not_to exceed_query_limit(0)

      expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
        .not_to exceed_query_limit(0)
    end
4044
  end
4045 4046

  describe '#version_params_for' do
4047 4048 4049
    let(:project) { create(:project, :repository) }

    subject { create(:merge_request, importing: true, source_project: project) }
4050

4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078
    let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
    let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
    let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }

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

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

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

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

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

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

4080
  describe '#fetch_ref!' do
4081 4082
    let(:project) { create(:project, :repository) }

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

micael.bergeron's avatar
micael.bergeron committed
4085
    it 'fetches the ref correctly' do
4086
      expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
4087

4088 4089
      subject.fetch_ref!
      expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
4090 4091
    end
  end
4092 4093 4094 4095 4096 4097 4098 4099 4100

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

      expect { subject.destroy }
        .to change { project.open_merge_requests_count }.from(1).to(0)
    end
  end
4101 4102 4103 4104

  it_behaves_like 'throttled touch' do
    subject { create(:merge_request, updated_at: 1.hour.ago) }
  end
4105 4106

  context 'state machine transitions' do
4107 4108
    let(:project) { create(:project, :repository) }

4109
    describe '#unlock_mr' do
4110
      subject { create(:merge_request, state: 'locked', source_project: project, merge_jid: 123) }
4111

4112
      it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do
4113 4114 4115 4116 4117 4118 4119 4120 4121
        pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)

        subject.unlock_mr

        subject.reload
        expect(subject.head_pipeline).to eq(pipeline)
        expect(subject.merge_jid).to be_nil
      end
    end
4122

4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181
    describe '#mark_as_unchecked' do
      subject { create(:merge_request, source_project: project, merge_status: merge_status) }

      shared_examples 'for an invalid state transition' do
        it 'is not a valid state transition' do
          expect { subject.mark_as_unchecked! }.to raise_error(StateMachines::InvalidTransition)
        end
      end

      shared_examples 'for an valid state transition' do
        it 'is a valid state transition' do
          expect { subject.mark_as_unchecked! }
            .to change { subject.merge_status }
            .from(merge_status.to_s)
            .to(expected_merge_status)
        end
      end

      context 'when the status is unchecked' do
        let(:merge_status) { :unchecked }

        include_examples 'for an invalid state transition'
      end

      context 'when the status is checking' do
        let(:merge_status) { :checking }
        let(:expected_merge_status) { 'unchecked' }

        include_examples 'for an valid state transition'
      end

      context 'when the status is can_be_merged' do
        let(:merge_status) { :can_be_merged }
        let(:expected_merge_status) { 'unchecked' }

        include_examples 'for an valid state transition'
      end

      context 'when the status is cannot_be_merged_recheck' do
        let(:merge_status) { :cannot_be_merged_recheck }

        include_examples 'for an invalid state transition'
      end

      context 'when the status is cannot_be_merged' do
        let(:merge_status) { :cannot_be_merged }
        let(:expected_merge_status) { 'cannot_be_merged_recheck' }

        include_examples 'for an valid state transition'
      end

      context 'when the status is cannot_be_merged' do
        let(:merge_status) { :cannot_be_merged }
        let(:expected_merge_status) { 'cannot_be_merged_recheck' }

        include_examples 'for an valid state transition'
      end
    end

4182 4183 4184
    describe 'transition to cannot_be_merged' do
      let(:notification_service) { double(:notification_service) }
      let(:todo_service) { double(:todo_service) }
4185

4186
      subject { create(:merge_request, state, source_project: project, merge_status: :unchecked) }
4187 4188 4189 4190

      before do
        allow(NotificationService).to receive(:new).and_return(notification_service)
        allow(TodoService).to receive(:new).and_return(todo_service)
4191 4192

        allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
4193 4194
      end

4195 4196 4197
      [:opened, :locked].each do |state|
        context state do
          let(:state) { state }
4198

4199 4200 4201 4202
          it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
            expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
            expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once

4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218
            subject.mark_as_unmergeable!

            subject.mark_as_unchecked!
            subject.mark_as_unmergeable!
          end

          it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged with async mergeability check' do
            expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
            expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once

            subject.mark_as_checking!
            subject.mark_as_unmergeable!

            subject.mark_as_unchecked!
            subject.mark_as_checking!
            subject.mark_as_unmergeable!
4219 4220 4221 4222 4223 4224
          end

          it 'notifies conflict, whenever newly unmergeable' do
            expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
            expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice

4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247
            subject.mark_as_unmergeable!

            subject.mark_as_unchecked!
            subject.mark_as_mergeable!

            subject.mark_as_unchecked!
            subject.mark_as_unmergeable!
          end

          it 'notifies conflict, whenever newly unmergeable with async mergeability check' do
            expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
            expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice

            subject.mark_as_checking!
            subject.mark_as_unmergeable!

            subject.mark_as_unchecked!
            subject.mark_as_checking!
            subject.mark_as_mergeable!

            subject.mark_as_unchecked!
            subject.mark_as_checking!
            subject.mark_as_unmergeable!
4248 4249 4250 4251
          end

          it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
            allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
4252

4253 4254
            expect(notification_service).not_to receive(:merge_request_unmergeable)
            expect(todo_service).not_to receive(:merge_request_became_unmergeable)
4255

4256
            subject.mark_as_unmergeable!
4257 4258
          end
        end
4259
      end
4260

4261 4262
      [:closed, :merged].each do |state|
        let(:state) { state }
4263

4264 4265 4266 4267
        context state do
          it 'does not notify' do
            expect(notification_service).not_to receive(:merge_request_unmergeable)
            expect(todo_service).not_to receive(:merge_request_became_unmergeable)
4268

4269
            subject.mark_as_unmergeable!
4270 4271
          end
        end
4272
      end
4273 4274

      context 'source branch is missing' do
4275
        subject { create(:merge_request, :invalid, :opened, source_project: project, merge_status: :unchecked, target_branch: 'master') }
4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288

        before do
          allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
        end

        it 'does not raise error' do
          expect(notification_service).not_to receive(:merge_request_unmergeable)
          expect(todo_service).not_to receive(:merge_request_became_unmergeable)

          expect { subject.mark_as_unmergeable }.not_to raise_error
          expect(subject.cannot_be_merged?).to eq(true)
        end
      end
4289 4290 4291 4292 4293
    end

    describe 'check_state?' do
      it 'indicates whether MR is still checking for mergeability' do
        state_machine = described_class.state_machines[:merge_status]
4294
        check_states = [:unchecked, :cannot_be_merged_recheck, :cannot_be_merged_rechecking, :checking]
4295 4296 4297 4298 4299 4300 4301 4302 4303 4304

        check_states.each do |merge_status|
          expect(state_machine.check_state?(merge_status)).to be true
        end

        (state_machine.states.map(&:name) - check_states).each do |merge_status|
          expect(state_machine.check_state?(merge_status)).to be false
        end
      end
    end
4305
  end
4306 4307 4308

  describe '#should_be_rebased?' do
    it 'returns false for the same source and target branches' do
4309
      merge_request = build_stubbed(:merge_request, source_project: project, target_project: project)
4310 4311 4312 4313 4314 4315

      expect(merge_request.should_be_rebased?).to be_falsey
    end
  end

  describe '#rebase_in_progress?' do
4316 4317 4318 4319 4320 4321
    where(:rebase_jid, :jid_valid, :result) do
      'foo' | true  | true
      'foo' | false | false
      ''    | true  | false
      nil   | true  | false
    end
4322

4323
    with_them do
4324
      let(:merge_request) { build_stubbed(:merge_request) }
4325 4326 4327 4328 4329 4330 4331 4332 4333

      subject { merge_request.rebase_in_progress? }

      it do
        allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }

        merge_request.rebase_jid = rebase_jid

        is_expected.to eq(result)
4334
      end
4335 4336
    end
  end
4337

4338
  describe '#allow_collaboration' do
4339
    let(:merge_request) do
4340
      build(:merge_request, source_branch: 'fixes', allow_collaboration: true)
4341 4342 4343
    end

    it 'is false when pushing by a maintainer is not possible' do
4344
      expect(merge_request).to receive(:collaborative_push_possible?) { false }
4345

4346
      expect(merge_request.allow_collaboration).to be_falsy
4347 4348 4349
    end

    it 'is true when pushing by a maintainer is possible' do
4350
      expect(merge_request).to receive(:collaborative_push_possible?) { true }
4351

4352
      expect(merge_request.allow_collaboration).to be_truthy
4353 4354 4355
    end
  end

4356
  describe '#collaborative_push_possible?' do
4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367
    let(:merge_request) do
      build(:merge_request, source_branch: 'fixes')
    end

    before do
      allow(ProtectedBranch).to receive(:protected?) { false }
    end

    it 'does not allow maintainer to push if the source project is the same as the target' do
      merge_request.target_project = merge_request.source_project = create(:project, :public)

4368
      expect(merge_request.collaborative_push_possible?).to be_falsy
4369 4370 4371 4372 4373 4374
    end

    it 'allows maintainer to push when both source and target are public' do
      merge_request.target_project = build(:project, :public)
      merge_request.source_project = build(:project, :public)

4375
      expect(merge_request.collaborative_push_possible?).to be_truthy
4376 4377 4378 4379 4380 4381 4382 4383 4384 4385
    end

    it 'is not available for protected branches' do
      merge_request.target_project = build(:project, :public)
      merge_request.source_project = build(:project, :public)

      expect(ProtectedBranch).to receive(:protected?)
                                   .with(merge_request.source_project, 'fixes')
                                   .and_return(true)

4386
      expect(merge_request.collaborative_push_possible?).to be_falsy
4387 4388 4389
    end
  end

4390
  describe '#can_allow_collaboration?' do
4391 4392 4393 4394 4395 4396 4397 4398
    let(:target_project) { create(:project, :public) }
    let(:source_project) { fork_project(target_project) }
    let(:merge_request) do
      create(:merge_request,
             source_project: source_project,
             source_branch: 'fixes',
             target_project: target_project)
    end
4399

4400 4401 4402
    let(:user) { create(:user) }

    before do
4403
      allow(merge_request).to receive(:collaborative_push_possible?) { true }
4404 4405 4406
    end

    it 'is false if the user does not have push access to the source project' do
4407
      expect(merge_request.can_allow_collaboration?(user)).to be_falsy
4408 4409 4410 4411 4412
    end

    it 'is true when the user has push access to the source project' do
      source_project.add_developer(user)

4413
      expect(merge_request.can_allow_collaboration?(user)).to be_truthy
4414 4415
    end
  end
4416 4417 4418

  describe '#merge_participants' do
    it 'contains author' do
4419
      expect(subject.merge_participants).to contain_exactly(subject.author)
4420 4421 4422 4423 4424
    end

    describe 'when merge_when_pipeline_succeeds? is true' do
      describe 'when merge user is author' do
        let(:user) { create(:user) }
4425

4426 4427 4428 4429 4430 4431 4432
        subject do
          create(:merge_request,
                 merge_when_pipeline_succeeds: true,
                 merge_user: user,
                 author: user)
        end

4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446
        context 'author is not a project member' do
          it 'is empty' do
            expect(subject.merge_participants).to be_empty
          end
        end

        context 'author is a project member' do
          before do
            subject.project.team.add_reporter(user)
          end

          it 'contains author only' do
            expect(subject.merge_participants).to contain_exactly(subject.author)
          end
4447 4448 4449 4450 4451
        end
      end

      describe 'when merge user and author are different users' do
        let(:merge_user) { create(:user) }
4452

4453 4454 4455 4456 4457 4458
        subject do
          create(:merge_request,
                 merge_when_pipeline_succeeds: true,
                 merge_user: merge_user)
        end

4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476
        before do
          subject.project.team.add_reporter(subject.author)
        end

        context 'merge user is not a member' do
          it 'contains author only' do
            expect(subject.merge_participants).to contain_exactly(subject.author)
          end
        end

        context 'both author and merge users are project members' do
          before do
            subject.project.team.add_reporter(merge_user)
          end

          it 'contains author and merge user' do
            expect(subject.merge_participants).to contain_exactly(subject.author, merge_user)
          end
4477 4478 4479 4480
        end
      end
    end
  end
4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508

  describe '.merge_request_ref?' do
    subject { described_class.merge_request_ref?(ref) }

    context 'when ref is ref name of a branch' do
      let(:ref) { 'feature' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is HEAD ref path of a branch' do
      let(:ref) { 'refs/heads/feature' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is HEAD ref path of a merge request' do
      let(:ref) { 'refs/merge-requests/1/head' }

      it { is_expected.to be_truthy }
    end

    context 'when ref is merge ref path of a merge request' do
      let(:ref) { 'refs/merge-requests/1/merge' }

      it { is_expected.to be_truthy }
    end
  end
4509

4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543
  describe '.merge_train_ref?' do
    subject { described_class.merge_train_ref?(ref) }

    context 'when ref is ref name of a branch' do
      let(:ref) { 'feature' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is HEAD ref path of a branch' do
      let(:ref) { 'refs/heads/feature' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is HEAD ref path of a merge request' do
      let(:ref) { 'refs/merge-requests/1/head' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is merge ref path of a merge request' do
      let(:ref) { 'refs/merge-requests/1/merge' }

      it { is_expected.to be_falsey }
    end

    context 'when ref is train ref path of a merge request' do
      let(:ref) { 'refs/merge-requests/1/train' }

      it { is_expected.to be_truthy }
    end
  end

4544 4545 4546
  describe '#cleanup_refs' do
    subject { merge_request.cleanup_refs(only: only) }

4547
    let(:merge_request) { build(:merge_request, source_project: create(:project, :repository)) }
4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572

    context 'when removing all refs' do
      let(:only) { :all }

      it 'deletes all refs from the target project' do
        expect(merge_request.target_project.repository)
          .to receive(:delete_refs)
          .with(merge_request.ref_path, merge_request.merge_ref_path, merge_request.train_ref_path)

        subject
      end
    end

    context 'when removing only train ref' do
      let(:only) { :train }

      it 'deletes train ref from the target project' do
        expect(merge_request.target_project.repository)
          .to receive(:delete_refs)
          .with(merge_request.train_ref_path)

        subject
      end
    end
  end
4573

4574
  describe '.with_auto_merge_enabled' do
4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593
    let!(:project) { create(:project) }
    let!(:fork) { fork_project(project) }
    let!(:merge_request1) do
      create(:merge_request,
             :merge_when_pipeline_succeeds,
             target_project: project,
             target_branch: 'master',
             source_project: project,
             source_branch: 'feature-1')
    end

    let!(:merge_request4) do
      create(:merge_request,
             target_project: project,
             target_branch: 'master',
             source_project: fork,
             source_branch: 'fork-feature-2')
    end

4594
    let(:query) { described_class.with_auto_merge_enabled }
4595

4596
    it { expect(query).to contain_exactly(merge_request1) }
4597
  end
4598 4599

  it_behaves_like 'versioned description'
4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662

  describe '#commits' do
    context 'persisted merge request' do
      context 'with a limit' do
        it 'returns a limited number of commits' do
          expect(subject.commits(limit: 2).map(&:sha)).to eq(%w[
            b83d6e391c22777fca1ed3012fce84f633d7fed0
            498214de67004b1da3d820901307bed2a68a8ef6
          ])
          expect(subject.commits(limit: 3).map(&:sha)).to eq(%w[
            b83d6e391c22777fca1ed3012fce84f633d7fed0
            498214de67004b1da3d820901307bed2a68a8ef6
            1b12f15a11fc6e62177bef08f47bc7b5ce50b141
          ])
        end
      end

      context 'without a limit' do
        it 'returns all commits of the merge request diff' do
          expect(subject.commits.size).to eq(29)
        end
      end
    end

    context 'new merge request' do
      subject { build(:merge_request) }

      context 'compare commits' do
        let(:first_commit) { double }
        let(:second_commit) { double }

        before do
          subject.compare_commits = [
            first_commit, second_commit
          ]
        end

        context 'without a limit' do
          it 'returns all the compare commits' do
            expect(subject.commits.to_a).to eq([second_commit, first_commit])
          end
        end

        context 'with a limit' do
          it 'returns a limited number of commits' do
            expect(subject.commits(limit: 1).to_a).to eq([second_commit])
          end
        end
      end
    end
  end

  describe '#recent_commits' do
    before do
      stub_const("#{MergeRequestDiff}::COMMITS_SAFE_SIZE", 2)
    end

    it 'returns the safe number of commits' do
      expect(subject.recent_commits.map(&:sha)).to eq(%w[
        b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
      ])
    end
  end
4663 4664 4665 4666 4667

  describe '#recent_visible_deployments' do
    let(:merge_request) { create(:merge_request) }

    it 'returns visible deployments' do
4668 4669
      envs = create_list(:environment, 3, project: merge_request.target_project)

4670 4671 4672 4673
      created = create(
        :deployment,
        :created,
        project: merge_request.target_project,
4674
        environment: envs[0]
4675 4676 4677 4678 4679 4680
      )

      success = create(
        :deployment,
        :success,
        project: merge_request.target_project,
4681
        environment: envs[1]
4682 4683 4684 4685 4686 4687
      )

      failed = create(
        :deployment,
        :failed,
        project: merge_request.target_project,
4688
        environment: envs[2]
4689 4690
      )

4691 4692 4693 4694
      merge_request_relation = MergeRequest.where(id: merge_request.id)
      created.link_merge_requests(merge_request_relation)
      success.link_merge_requests(merge_request_relation)
      failed.link_merge_requests(merge_request_relation)
4695 4696 4697 4698 4699 4700

      expect(merge_request.recent_visible_deployments).to eq([failed, success])
    end

    it 'only returns a limited number of deployments' do
      20.times do
4701
        environment = create(:environment, project: merge_request.target_project)
4702 4703 4704 4705 4706 4707 4708
        deploy = create(
          :deployment,
          :success,
          project: merge_request.target_project,
          environment: environment
        )

4709
        deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
4710 4711 4712 4713 4714
      end

      expect(merge_request.recent_visible_deployments.count).to eq(10)
    end
  end
4715 4716

  describe '#diffable_merge_ref?' do
4717 4718
    let(:merge_request) { create(:merge_request) }

4719
    context 'merge request can be merged' do
4720
      context 'merge_head diff is not created' do
4721
        it 'returns true' do
4722
          expect(merge_request.diffable_merge_ref?).to eq(false)
4723 4724 4725
        end
      end

4726
      context 'merge_head diff is created' do
4727
        before do
4728
          create(:merge_request_diff, :merge_head, merge_request: merge_request)
4729
        end
4730

4731
        it 'returns true' do
4732
          expect(merge_request.diffable_merge_ref?).to eq(true)
4733 4734
        end

4735
        context 'merge request is merged' do
4736 4737 4738
          before do
            merge_request.mark_as_merged!
          end
4739 4740

          it 'returns false' do
4741
            expect(merge_request.diffable_merge_ref?).to eq(false)
4742 4743 4744
          end
        end

4745 4746
        context 'merge request cannot be merged' do
          before do
4747
            merge_request.mark_as_unchecked!
4748 4749 4750
          end

          it 'returns false' do
4751
            expect(merge_request.diffable_merge_ref?).to eq(true)
4752 4753 4754 4755 4756 4757
          end

          context 'display_merge_conflicts_in_diff is disabled' do
            before do
              stub_feature_flags(display_merge_conflicts_in_diff: false)
            end
4758

4759
            it 'returns false' do
4760
              expect(merge_request.diffable_merge_ref?).to eq(false)
4761 4762 4763
            end
          end
        end
4764 4765 4766
      end
    end
  end
4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780

  describe '#predefined_variables' do
    let(:merge_request) { create(:merge_request) }

    it 'caches all SQL-sourced data on the first call' do
      control = ActiveRecord::QueryRecorder.new { merge_request.predefined_variables }.count

      expect(control).to be > 0

      count = ActiveRecord::QueryRecorder.new { merge_request.predefined_variables }.count

      expect(count).to eq(0)
    end
  end
4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791

  describe 'banzai_render_context' do
    let(:project) { build(:project_empty_repo) }
    let(:merge_request) { build :merge_request, target_project: project, source_project: project }

    subject(:context) { merge_request.banzai_render_context(:title) }

    it 'sets the label_url_method in the context' do
      expect(context[:label_url_method]).to eq(:project_merge_requests_url)
    end
  end
4792

4793
  describe '#head_pipeline_builds_with_coverage' do
4794 4795
    it 'delegates to head_pipeline' do
      expect(subject)
4796 4797 4798 4799
        .to delegate_method(:builds_with_coverage)
        .to(:head_pipeline)
        .with_prefix
        .with_arguments(allow_nil: true)
4800 4801
    end
  end
4802

Patrick Bajao's avatar
Patrick Bajao committed
4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827
  describe '#merge_ref_head' do
    let(:merge_request) { create(:merge_request) }

    context 'when merge_ref_sha is not present' do
      let!(:result) do
        MergeRequests::MergeToRefService
          .new(merge_request.project, merge_request.author)
          .execute(merge_request)
      end

      it 'returns the commit based on merge ref path' do
        expect(merge_request.merge_ref_head.id).to eq(result[:commit_id])
      end
    end

    context 'when merge_ref_sha is present' do
      before do
        merge_request.update!(merge_ref_sha: merge_request.project.repository.commit.id)
      end

      it 'returns the commit based on cached merge_ref_sha' do
        expect(merge_request.merge_ref_head.id).to eq(merge_request.merge_ref_sha)
      end
    end
  end
4828 4829

  describe '#allows_reviewers?' do
4830
    it 'returns true' do
4831 4832 4833 4834 4835
      merge_request = build_stubbed(:merge_request)

      expect(merge_request.allows_reviewers?).to be(true)
    end
  end
4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849

  describe '#update_and_mark_in_progress_merge_commit_sha' do
    let(:ref) { subject.target_project.repository.commit.id }

    before do
      expect(subject.target_project).to receive(:mark_primary_write_location)
    end

    it 'updates commit ID' do
      expect { subject.update_and_mark_in_progress_merge_commit_sha(ref) }
        .to change { subject.in_progress_merge_commit_sha }
        .from(nil).to(ref)
    end
  end
4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879

  describe '#enabled_reports' do
    let(:project) { create(:project, :repository) }

    where(:report_type, :with_reports, :feature) do
      :sast                | :with_sast_reports                | :sast
      :secret_detection    | :with_secret_detection_reports    | :secret_detection
    end

    with_them do
      subject { merge_request.enabled_reports[report_type] }

      before do
        stub_feature_flags(drop_license_management_artifact: false)
        stub_licensed_features({ feature => true })
      end

      context "when head pipeline has reports" do
        let(:merge_request) { create(:merge_request, with_reports, source_project: project) }

        it { is_expected.to be_truthy }
      end

      context "when head pipeline does not have reports" do
        let(:merge_request) { create(:merge_request, source_project: project) }

        it { is_expected.to be_falsy }
      end
    end
  end
4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908

  describe '#includes_ci_config?' do
    let(:merge_request) { build(:merge_request) }
    let(:project) { merge_request.project }

    subject(:result) { merge_request.includes_ci_config? }

    before do
      allow(merge_request).to receive(:diff_stats).and_return(diff_stats)
    end

    context 'when diff_stats is nil' do
      let(:diff_stats) {}

      it { is_expected.to eq(false) }
    end

    context 'when diff_stats does not include the ci config path of the project' do
      let(:diff_stats) { [double(path: 'abc.txt')] }

      it { is_expected.to eq(false) }
    end

    context 'when diff_stats includes the ci config path of the project' do
      let(:diff_stats) { [double(path: '.gitlab-ci.yml')] }

      it { is_expected.to eq(true) }
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
4909
end