system_note_service_spec.rb 37.4 KB
Newer Older
1 2
require 'spec_helper'

3
describe SystemNoteService do
4
  include Gitlab::Routing
5
  include RepoHelpers
6
  include AssetsHelpers
7

8 9 10
  set(:group)    { create(:group) }
  set(:project)  { create(:project, :repository, group: group) }
  set(:author)   { create(:user) }
11
  let(:noteable) { create(:issue, project: project) }
12
  let(:issue)    { noteable }
13 14

  shared_examples_for 'a system note' do
15 16 17
    let(:expected_noteable) { noteable }
    let(:commit_count)      { nil }

18
    it 'has the correct attributes', :aggregate_failures do
19
      expect(subject).to be_valid
20
      expect(subject).to be_system
21

22
      expect(subject.noteable).to eq expected_noteable
23 24
      expect(subject.project).to eq project
      expect(subject.author).to eq author
25

26 27
      expect(subject.system_note_metadata.action).to eq(action)
      expect(subject.system_note_metadata.commit_count).to eq(commit_count)
28
    end
29 30
  end

31
  describe '.add_commits' do
32 33
    subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) }

34
    let(:noteable)    { create(:merge_request, source_project: project, target_project: project) }
35 36 37
    let(:new_commits) { noteable.commits }
    let(:old_commits) { [] }
    let(:oldrev)      { nil }
38

39 40 41 42
    it_behaves_like 'a system note' do
      let(:commit_count) { new_commits.size }
      let(:action)       { 'commit' }
    end
43

44 45
    describe 'note body' do
      let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
46

47
      describe 'comparison diff link line' do
48 49
        it 'adds the comparison text' do
          expect(note_lines[2]).to match "[Compare with previous version]"
50 51 52
        end
      end

53 54
      context 'without existing commits' do
        it 'adds a message header' do
55
          expect(note_lines[0]).to eq "added #{new_commits.size} commits"
56
        end
57

58 59 60 61 62
        it 'adds a message for each commit' do
          decoded_note_content = HTMLEntities.new.decode(subject.note)

          new_commits.each do |commit|
            expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
63 64
          end
        end
65 66
      end

67
      describe 'summary line for existing commits' do
68
        let(:summary_line) { note_lines[1] }
69

70 71
        context 'with one existing commit' do
          let(:old_commits) { [noteable.commits.last] }
72

73
          it 'includes the existing commit' do
74
            expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
75 76
          end
        end
77

78 79
        context 'with multiple existing commits' do
          let(:old_commits) { noteable.commits[3..-1] }
80

81 82
          context 'with oldrev' do
            let(:oldrev) { noteable.commits[2].id }
83

84 85 86
            it 'includes a commit range and count' do
              expect(summary_line)
                .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
87 88 89
            end
          end

90
          context 'without oldrev' do
91 92 93
            it 'includes a commit range and count' do
              expect(summary_line)
                .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
94 95 96
            end
          end

97 98 99 100 101 102
          context 'on a fork' do
            before do
              expect(noteable).to receive(:for_fork?).and_return(true)
            end

            it 'includes the project namespace' do
103
              expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>")
104 105 106 107 108 109 110
            end
          end
        end
      end
    end
  end

111 112 113 114
  describe '.tag_commit' do
    let(:noteable) do
      project.commit
    end
115
    let(:tag_name) { 'v1.2.3' }
116 117 118 119 120 121 122 123

    subject { described_class.tag_commit(noteable, project, author, tag_name) }

    it_behaves_like 'a system note' do
      let(:action) { 'tag' }
    end

    it 'sets the note text' do
124 125 126
      link = "http://localhost/#{project.full_path}/tags/#{tag_name}"

      expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
127 128 129
    end
  end

130 131 132
  describe '.change_assignee' do
    subject { described_class.change_assignee(noteable, project, author, assignee) }

133 134
    let(:assignee) { create(:user) }

135 136 137
    it_behaves_like 'a system note' do
      let(:action) { 'assignee' }
    end
138 139 140

    context 'when assignee added' do
      it 'sets the note text' do
141
        expect(subject.note).to eq "assigned to @#{assignee.username}"
142 143 144 145 146 147 148
      end
    end

    context 'when assignee removed' do
      let(:assignee) { nil }

      it 'sets the note text' do
149
        expect(subject.note).to eq 'removed assignee'
150 151 152 153
      end
    end
  end

154 155 156 157 158 159 160 161
  describe '.change_issue_assignees' do
    subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }

    let(:assignee) { create(:user) }
    let(:assignee1) { create(:user) }
    let(:assignee2) { create(:user) }
    let(:assignee3) { create(:user) }

162 163 164
    it_behaves_like 'a system note' do
      let(:action) { 'assignee' }
    end
165 166 167 168 169 170 171 172 173 174 175

    def build_note(old_assignees, new_assignees)
      issue.assignees = new_assignees
      described_class.change_issue_assignees(issue, project, author, old_assignees).note
    end

    it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
      expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
    end

    it 'builds a correct phrase when assignee removed' do
176
      expect(build_note([assignee1], [])).to eq "unassigned @#{assignee1.username}"
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
    end

    it 'builds a correct phrase when assignees changed' do
      expect(build_note([assignee1], [assignee2])).to eq \
        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
    end

    it 'builds a correct phrase when three assignees removed and one added' do
      expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
        "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
    end

    it 'builds a correct phrase when one assignee changed from a set' do
      expect(build_note([assignee, assignee1], [assignee, assignee2])).to eq \
        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
    end

    it 'builds a correct phrase when one assignee removed from a set' do
      expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \
        "unassigned @#{assignee2.username}"
    end
  end

200
  describe '.change_milestone' do
201 202
    context 'for a project milestone' do
      subject { described_class.change_milestone(noteable, project, author, milestone) }
203

204
      let(:milestone) { create(:milestone, project: project) }
205

206 207 208
      it_behaves_like 'a system note' do
        let(:action) { 'milestone' }
      end
209

210 211
      context 'when milestone added' do
        it 'sets the note text' do
212 213 214
          reference = milestone.to_reference(format: :iid)

          expect(subject.note).to eq "changed milestone to #{reference}"
215 216 217 218 219 220 221 222 223
        end
      end

      context 'when milestone removed' do
        let(:milestone) { nil }

        it 'sets the note text' do
          expect(subject.note).to eq 'removed milestone'
        end
224 225 226
      end
    end

227 228
    context 'for a group milestone' do
      subject { described_class.change_milestone(noteable, project, author, milestone) }
229

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
      let(:milestone) { create(:milestone, group: group) }

      it_behaves_like 'a system note' do
        let(:action) { 'milestone' }
      end

      context 'when milestone added' do
        it 'sets the note text to use the milestone name' do
          expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
        end
      end

      context 'when milestone removed' do
        let(:milestone) { nil }

        it 'sets the note text' do
          expect(subject.note).to eq 'removed milestone'
        end
248 249 250 251
      end
    end
  end

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
  describe '.change_due_date' do
    subject { described_class.change_due_date(noteable, project, author, due_date) }

    let(:due_date) { Date.today }

    it_behaves_like 'a system note' do
      let(:action) { 'due_date' }
    end

    context 'when due date added' do
      it 'sets the note text' do
        expect(subject.note).to eq "changed due date to #{Date.today.to_s(:long)}"
      end
    end

    context 'when due date removed' do
      let(:due_date) { nil }

      it 'sets the note text' do
        expect(subject.note).to eq 'removed due date'
      end
    end
  end

276
  describe '.change_status' do
277 278
    subject { described_class.change_status(noteable, project, author, status, source) }

279 280 281
    context 'with status reopened' do
      let(:status) { 'reopened' }
      let(:source) { nil }
282

283 284 285
      it_behaves_like 'a system note' do
        let(:action) { 'opened' }
      end
286
    end
287

288
    context 'with a source' do
289
      let(:status) { 'opened' }
290
      let(:source) { double('commit', gfm_reference: 'commit 123456') }
291

292
      it 'sets the note text' do
293
        expect(subject.note).to eq "#{status} via commit 123456"
294 295 296
      end
    end
  end
297

298
  describe '.merge_when_pipeline_succeeds' do
299
    let(:pipeline) { build(:ci_pipeline_without_jobs )}
300 301 302
    let(:noteable) do
      create(:merge_request, source_project: project, target_project: project)
    end
303

304
    subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) }
305

306 307 308
    it_behaves_like 'a system note' do
      let(:action) { 'merge' }
    end
309

310
    it "posts the 'merge when pipeline succeeds' system note" do
311
      expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds})
312 313 314
    end
  end

315
  describe '.cancel_merge_when_pipeline_succeeds' do
316 317 318
    let(:noteable) do
      create(:merge_request, source_project: project, target_project: project)
    end
319

320
    subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) }
321

322 323 324
    it_behaves_like 'a system note' do
      let(:action) { 'merge' }
    end
325

326
    it "posts the 'merge when pipeline succeeds' system note" do
Semyon Pupkov's avatar
Semyon Pupkov committed
327
      expect(subject.note).to eq "canceled the automatic merge"
328 329 330
    end
  end

331
  describe '.change_title' do
332 333
    let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }

334 335 336
    subject { described_class.change_title(noteable, project, author, 'Old title') }

    context 'when noteable responds to `title`' do
337 338 339
      it_behaves_like 'a system note' do
        let(:action) { 'title' }
      end
340 341

      it 'sets the note text' do
342 343
        expect(subject.note)
          .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
344 345 346 347
      end
    end
  end

348 349 350 351 352 353 354 355 356
  describe '.change_description' do
    subject { described_class.change_description(noteable, project, author) }

    context 'when noteable responds to `description`' do
      it_behaves_like 'a system note' do
        let(:action) { 'description' }
      end

      it 'sets the note text' do
blackst0ne's avatar
blackst0ne committed
357
        expect(subject.note).to eq('changed the description')
358 359 360 361
      end
    end
  end

362 363
  describe '.change_issue_confidentiality' do
    subject { described_class.change_issue_confidentiality(noteable, project, author) }
364

365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
    context 'issue has been made confidential' do
      before do
        noteable.update_attribute(:confidential, true)
      end

      it_behaves_like 'a system note' do
        let(:action) { 'confidential' }
      end

      it 'sets the note text' do
        expect(subject.note).to eq 'made the issue confidential'
      end
    end

    context 'issue has been made visible' do
380
      it_behaves_like 'a system note' do
381
        let(:action) { 'visible' }
382
      end
383 384

      it 'sets the note text' do
385
        expect(subject.note).to eq 'made the issue visible to everyone'
386 387 388 389
      end
    end
  end

390
  describe '.change_branch' do
391
    subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) }
392

393 394 395
    let(:old_branch) { 'old_branch'}
    let(:new_branch) { 'new_branch'}

396 397 398
    it_behaves_like 'a system note' do
      let(:action) { 'branch' }
    end
399 400 401

    context 'when target branch name changed' do
      it 'sets the note text' do
402
        expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
403 404 405 406
      end
    end
  end

407
  describe '.change_branch_presence' do
408
    subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) }
409

410 411 412
    it_behaves_like 'a system note' do
      let(:action) { 'branch' }
    end
413 414 415

    context 'when source branch deleted' do
      it 'sets the note text' do
416
        expect(subject.note).to eq "deleted source branch `feature`"
417 418 419 420
      end
    end
  end

421 422 423
  describe '.new_issue_branch' do
    subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }

424 425 426
    it_behaves_like 'a system note' do
      let(:action) { 'branch' }
    end
427 428 429

    context 'when a branch is created from the new branch button' do
      it 'sets the note text' do
430
        expect(subject.note).to start_with("created branch [`1-mepmep`]")
431 432 433 434
      end
    end
  end

435 436 437 438 439 440 441 442 443 444 445 446 447 448
  describe '.new_merge_request' do
    subject { described_class.new_merge_request(noteable, project, author, merge_request) }

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

    it_behaves_like 'a system note' do
      let(:action) { 'merge' }
    end

    it 'sets the new merge request note text' do
      expect(subject.note).to eq("created merge request #{merge_request.to_reference} to address this issue")
    end
  end

449 450
  describe '.cross_reference' do
    subject { described_class.cross_reference(noteable, mentioner, author) }
451

452 453
    let(:mentioner) { create(:issue, project: project) }

454 455 456
    it_behaves_like 'a system note' do
      let(:action) { 'cross_reference' }
    end
457

458 459 460 461
    context 'when cross-reference disallowed' do
      before do
        expect(described_class).to receive(:cross_reference_disallowed?).and_return(true)
      end
462

463 464 465
      it 'returns nil' do
        expect(subject).to be_nil
      end
466 467

      it 'does not create a system note metadata record' do
468
        expect { subject }.not_to change { SystemNoteMetadata.count }
469
      end
470
    end
471

472 473 474 475
    context 'when cross-reference allowed' do
      before do
        expect(described_class).to receive(:cross_reference_disallowed?).and_return(false)
      end
476

477 478 479 480
      it_behaves_like 'a system note' do
        let(:action) { 'cross_reference' }
      end

481 482
      describe 'note_body' do
        context 'cross-project' do
483
          let(:project2) { create(:project, :repository) }
484
          let(:mentioner) { create(:issue, project: project2) }
485

486 487
          context 'from Commit' do
            let(:mentioner) { project2.repository.commit }
488

489
            it 'references the mentioning commit' do
490
              expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
491 492 493
            end
          end

494 495
          context 'from non-Commit' do
            it 'references the mentioning object' do
496
              expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
497 498 499 500
            end
          end
        end

501
        context 'within the same project' do
502 503
          context 'from Commit' do
            let(:mentioner) { project.repository.commit }
504

505
            it 'references the mentioning commit' do
506
              expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
507 508
            end
          end
509

510 511
          context 'from non-Commit' do
            it 'references the mentioning object' do
512
              expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
513 514 515
            end
          end
        end
516 517 518 519
      end
    end
  end

520 521 522 523
  describe '.cross_reference_disallowed?' do
    context 'when mentioner is not a MergeRequest' do
      it 'is falsey' do
        mentioner = noteable.dup
524 525
        expect(described_class.cross_reference_disallowed?(noteable, mentioner))
          .to be_falsey
526 527 528 529 530 531 532 533 534
      end
    end

    context 'when mentioner is a MergeRequest' do
      let(:mentioner) { create(:merge_request, :simple, source_project: project) }
      let(:noteable)  { project.commit }

      it 'is truthy when noteable is in commits' do
        expect(mentioner).to receive(:commits).and_return([noteable])
535 536
        expect(described_class.cross_reference_disallowed?(noteable, mentioner))
          .to be_truthy
537 538 539 540
      end

      it 'is falsey when noteable is not in commits' do
        expect(mentioner).to receive(:commits).and_return([])
541 542
        expect(described_class.cross_reference_disallowed?(noteable, mentioner))
          .to be_falsey
543 544
      end
    end
545 546 547 548 549

    context 'when notable is an ExternalIssue' do
      let(:noteable) { ExternalIssue.new('EXT-1234', project) }
      it 'is truthy' do
        mentioner = noteable.dup
550 551
        expect(described_class.cross_reference_disallowed?(noteable, mentioner))
          .to be_truthy
552 553
      end
    end
554
  end
555 556 557 558 559 560 561 562 563 564 565 566

  describe '.cross_reference_exists?' do
    let(:commit0) { project.commit }
    let(:commit1) { project.commit('HEAD~2') }

    context 'issue from commit' do
      before do
        # Mention issue (noteable) from commit0
        described_class.cross_reference(noteable, commit0, author)
      end

      it 'is truthy when already mentioned' do
567 568
        expect(described_class.cross_reference_exists?(noteable, commit0))
          .to be_truthy
569 570 571
      end

      it 'is falsey when not already mentioned' do
572 573
        expect(described_class.cross_reference_exists?(noteable, commit1))
          .to be_falsey
574
      end
575 576 577 578 579 580 581 582 583

      context 'legacy capitalized cross reference' do
        before do
          # Mention issue (noteable) from commit0
          system_note = described_class.cross_reference(noteable, commit0, author)
          system_note.update(note: system_note.note.capitalize)
        end

        it 'is truthy when already mentioned' do
584 585
          expect(described_class.cross_reference_exists?(noteable, commit0))
            .to be_truthy
586 587
        end
      end
588 589 590 591 592 593 594 595 596
    end

    context 'commit from commit' do
      before do
        # Mention commit1 from commit0
        described_class.cross_reference(commit0, commit1, author)
      end

      it 'is truthy when already mentioned' do
597 598
        expect(described_class.cross_reference_exists?(commit0, commit1))
          .to be_truthy
599 600 601
      end

      it 'is falsey when not already mentioned' do
602 603
        expect(described_class.cross_reference_exists?(commit1, commit0))
          .to be_falsey
604
      end
605 606 607 608 609 610 611 612 613

      context 'legacy capitalized cross reference' do
        before do
          # Mention commit1 from commit0
          system_note = described_class.cross_reference(commit0, commit1, author)
          system_note.update(note: system_note.note.capitalize)
        end

        it 'is truthy when already mentioned' do
614 615
          expect(described_class.cross_reference_exists?(commit0, commit1))
            .to be_truthy
616 617
        end
      end
618
    end
619

620
    context 'commit with cross-reference from fork' do
621
      let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
622 623 624 625
      let(:forked_project) { Projects::ForkService.new(project, author2).execute }
      let(:commit2) { forked_project.commit }

      before do
626
        described_class.cross_reference(noteable, commit0, author2)
627 628
      end

629
      it 'is true when a fork mentions an external issue' do
630 631
        expect(described_class.cross_reference_exists?(noteable, commit2))
            .to be true
632
      end
633 634 635 636 637 638 639 640

      context 'legacy capitalized cross reference' do
        before do
          system_note = described_class.cross_reference(noteable, commit0, author2)
          system_note.update(note: system_note.note.capitalize)
        end

        it 'is true when a fork mentions an external issue' do
641 642
          expect(described_class.cross_reference_exists?(noteable, commit2))
              .to be true
643 644
        end
      end
645
    end
646
  end
Drew Blessing's avatar
Drew Blessing committed
647

648
  describe '.noteable_moved' do
649
    let(:new_project) { create(:project) }
650 651
    let(:new_noteable) { create(:issue, project: new_project) }

652
    subject do
653
      described_class.noteable_moved(noteable, project, new_noteable, author, direction: direction)
654 655
    end

656
    shared_examples 'cross project mentionable' do
657
      include MarkupHelper
658

659
      it 'contains cross reference to new noteable' do
660 661 662
        expect(subject.note).to include cross_project_reference(new_project, new_noteable)
      end

663
      it 'mentions referenced noteable' do
664 665 666
        expect(subject.note).to include new_noteable.to_reference
      end

667
      it 'mentions referenced project' do
668
        expect(subject.note).to include new_project.full_path
669 670 671 672 673 674 675
      end
    end

    context 'moved to' do
      let(:direction) { :to }

      it_behaves_like 'cross project mentionable'
676 677 678
      it_behaves_like 'a system note' do
        let(:action) { 'moved' }
      end
679

680
      it 'notifies about noteable being moved to' do
681
        expect(subject.note).to match('moved to')
682
      end
683 684
    end

685 686 687 688
    context 'moved from' do
      let(:direction) { :from }

      it_behaves_like 'cross project mentionable'
689 690 691
      it_behaves_like 'a system note' do
        let(:action) { 'moved' }
      end
692

693
      it 'notifies about noteable being moved from' do
694
        expect(subject.note).to match('moved from')
695
      end
696 697
    end

698
    context 'invalid direction' do
699
      let(:direction) { :invalid }
700

701
      it 'raises error' do
702 703
        expect { subject }.to raise_error StandardError, /Invalid direction/
      end
704 705 706
    end
  end

707 708 709
  describe '.new_commit_summary' do
    it 'escapes HTML titles' do
      commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
710
      escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
micael.bergeron's avatar
micael.bergeron committed
711

712
      expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/))
713 714 715
    end
  end

Drew Blessing's avatar
Drew Blessing committed
716
  describe 'JIRA integration' do
717 718
    include JiraServiceHelper

719
    let(:project)         { create(:jira_project, :repository) }
720 721
    let(:author)          { create(:user) }
    let(:issue)           { create(:issue, project: project) }
722
    let(:merge_request)   { create(:merge_request, :simple, target_project: project, source_project: project) }
723 724 725 726
    let(:jira_issue)      { ExternalIssue.new("JIRA-1", project)}
    let(:jira_tracker)    { project.jira_service }
    let(:commit)          { project.commit }
    let(:comment_url)     { jira_api_comment_url(jira_issue.id) }
727
    let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." }
728

729 730 731 732
    before do
      stub_jira_urls(jira_issue.id)
      jira_service_settings
    end
Drew Blessing's avatar
Drew Blessing committed
733

734 735 736 737 738 739 740 741 742 743
    def cross_reference(type, link_exists = false)
      noteable = type == 'commit' ? commit : merge_request

      links = []
      if link_exists
        url = if type == 'commit'
                "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/commit/#{commit.id}"
              else
                "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/merge_requests/#{merge_request.iid}"
              end
744

745 746 747 748 749 750 751 752 753 754
        link = double(object: { 'url' => url })
        links << link
        expect(link).to receive(:save!)
      end

      allow(JIRA::Resource::Remotelink).to receive(:all).and_return(links)

      described_class.cross_reference(jira_issue, noteable, author)
    end

Douwe Maan's avatar
Douwe Maan committed
755
    noteable_types = %w(merge_requests commit)
756 757 758 759 760 761

    noteable_types.each do |type|
      context "when noteable is a #{type}" do
        it "blocks cross reference when #{type.underscore}_events is false" do
          jira_tracker.update("#{type}_events" => false)

762
          expect(cross_reference(type)).to eq("Events for #{type.pluralize.humanize.downcase} are disabled.")
763 764
        end

765
        it "creates cross reference when #{type.underscore}_events is true" do
766 767
          jira_tracker.update("#{type}_events" => true)

768 769 770 771 772 773 774
          expect(cross_reference(type)).to eq(success_message)
        end
      end

      context 'when a new cross reference is created' do
        it 'creates a new comment and remote link' do
          cross_reference(type)
Drew Blessing's avatar
Drew Blessing committed
775

776 777 778 779 780 781 782 783 784 785
          expect(WebMock).to have_requested(:post, jira_api_comment_url(jira_issue))
          expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue))
        end
      end

      context 'when a link exists' do
        it 'updates a link but does not create a new comment' do
          expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))

          cross_reference(type, true)
786 787 788
        end
      end
    end
Drew Blessing's avatar
Drew Blessing committed
789

790
    describe "new reference" do
791
      let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
792

793 794 795 796
      before do
        allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
      end

797 798 799
      context 'for commits' do
        it "creates comment" do
          result = described_class.cross_reference(jira_issue, commit, author)
Drew Blessing's avatar
Drew Blessing committed
800

801 802
          expect(result).to eq(success_message)
        end
803 804

        it "creates remote link" do
805
          described_class.cross_reference(jira_issue, commit, author)
806 807 808 809

          expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
            body: hash_including(
              GlobalID: "GitLab",
810
              relationship: 'mentioned on',
811
              object: {
812
                url: project_commit_url(project, commit),
813
                title: "Commit - #{commit.title}",
814
                icon: { title: "GitLab", url16x16: favicon_path },
815 816 817 818 819
                status: { resolved: false }
              }
            )
          ).once
        end
Drew Blessing's avatar
Drew Blessing committed
820 821
      end

822
      context 'for issues' do
823
        let(:issue) { create(:issue, project: project) }
Drew Blessing's avatar
Drew Blessing committed
824

825 826
        it "creates comment" do
          result = described_class.cross_reference(jira_issue, issue, author)
Drew Blessing's avatar
Drew Blessing committed
827

828 829
          expect(result).to eq(success_message)
        end
830 831

        it "creates remote link" do
832
          described_class.cross_reference(jira_issue, issue, author)
833 834 835 836

          expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
            body: hash_including(
              GlobalID: "GitLab",
837
              relationship: 'mentioned on',
838
              object: {
839
                url: project_issue_url(project, issue),
840
                title: "Issue - #{issue.title}",
841
                icon: { title: "GitLab", url16x16: favicon_path },
842 843 844 845 846
                status: { resolved: false }
              }
            )
          ).once
        end
847
      end
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863

      context 'for snippets' do
        let(:snippet) { create(:snippet, project: project) }

        it "creates comment" do
          result = described_class.cross_reference(jira_issue, snippet, author)

          expect(result).to eq(success_message)
        end

        it "creates remote link" do
          described_class.cross_reference(jira_issue, snippet, author)

          expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
            body: hash_including(
              GlobalID: "GitLab",
864
              relationship: 'mentioned on',
865
              object: {
866
                url: project_snippet_url(project, snippet),
867
                title: "Snippet - #{snippet.title}",
868
                icon: { title: "GitLab", url16x16: favicon_path },
869 870 871 872 873 874
                status: { resolved: false }
              }
            )
          ).once
        end
      end
875 876 877 878
    end

    describe "existing reference" do
      before do
879
        allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
880
        message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.full_path}|http://localhost/#{project.full_path}/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
881
        allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
Drew Blessing's avatar
Drew Blessing committed
882
      end
883

884 885
      it "does not return success message" do
        result = described_class.cross_reference(jira_issue, commit, author)
886

887 888
        expect(result).not_to eq(success_message)
      end
889 890 891 892 893 894 895

      it 'does not try to create comment and remote link' do
        subject

        expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
        expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
      end
Drew Blessing's avatar
Drew Blessing committed
896 897
    end
  end
898 899

  describe '.discussion_continued_in_issue' do
900
    let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
901 902 903 904 905 906 907
    let(:merge_request) { discussion.noteable }
    let(:issue) { create(:issue, project: project) }

    def reloaded_merge_request
      MergeRequest.find(merge_request.id)
    end

908 909 910 911 912
    subject { described_class.discussion_continued_in_issue(discussion, project, author, issue) }

    it_behaves_like 'a system note' do
      let(:expected_noteable) { discussion.first_note.noteable }
      let(:action)              { 'discussion' }
913 914 915 916
    end

    it 'creates a new note in the discussion' do
      # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
917
      expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
918 919 920
    end

    it 'mentions the created issue in the system note' do
921
      expect(subject.note).to include(issue.to_reference)
922 923
    end
  end
924 925 926 927

  describe '.change_time_estimate' do
    subject { described_class.change_time_estimate(noteable, project, author) }

928 929 930
    it_behaves_like 'a system note' do
      let(:action) { 'time_tracking' }
    end
931 932 933 934 935

    context 'with a time estimate' do
      it 'sets the note text' do
        noteable.update_attribute(:time_estimate, 277200)

936
        expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
937 938 939 940 941
      end
    end

    context 'without a time estimate' do
      it 'sets the note text' do
942
        expect(subject.note).to eq "removed time estimate"
943 944 945 946 947 948 949 950
      end
    end
  end

  describe '.change_time_spent' do
    # We need a custom noteable in order to the shared examples to be green.
    let(:noteable) do
      mr = create(:merge_request, source_project: project)
951
      mr.spend_time(duration: 360000, user_id: author.id)
952 953 954 955 956 957 958 959
      mr.save!
      mr
    end

    subject do
      described_class.change_time_spent(noteable, project, author)
    end

960 961 962
    it_behaves_like 'a system note' do
      let(:action) { 'time_tracking' }
    end
963 964 965 966 967

    context 'when time was added' do
      it 'sets the note text' do
        spend_time!(277200)

968
        expect(subject.note).to eq "added 1w 4d 5h of time spent"
969 970 971 972 973 974 975
      end
    end

    context 'when time was subtracted' do
      it 'sets the note text' do
        spend_time!(-277200)

976
        expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
977 978 979 980 981 982 983
      end
    end

    context 'when time was removed' do
      it 'sets the note text' do
        spend_time!(:reset)

984
        expect(subject.note).to eq "removed time spent"
985 986 987 988
      end
    end

    def spend_time!(seconds)
989
      noteable.spend_time(duration: seconds, user_id: author.id)
990 991 992
      noteable.save!
    end
  end
993

994 995 996
  describe '.handle_merge_request_wip' do
    context 'adding wip note' do
      let(:noteable) { create(:merge_request, source_project: project, title: 'WIP Lorem ipsum') }
997

998
      subject { described_class.handle_merge_request_wip(noteable, project, author) }
999

1000 1001 1002
      it_behaves_like 'a system note' do
        let(:action) { 'title' }
      end
1003

1004 1005 1006
      it 'sets the note text' do
        expect(subject.note).to eq 'marked as a **Work In Progress**'
      end
1007 1008
    end

1009 1010
    context 'removing wip note' do
      let(:noteable) { create(:merge_request, source_project: project, title: 'Lorem ipsum') }
1011

1012
      subject { described_class.handle_merge_request_wip(noteable, project, author) }
1013

1014 1015 1016
      it_behaves_like 'a system note' do
        let(:action) { 'title' }
      end
1017

1018 1019 1020
      it 'sets the note text' do
        expect(subject.note).to eq 'unmarked as a **Work In Progress**'
      end
1021 1022 1023
    end
  end

1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
  describe '.add_merge_request_wip_from_commit' do
    let(:noteable) do
      create(:merge_request, source_project: project, target_project: project)
    end

    subject do
      described_class.add_merge_request_wip_from_commit(
        noteable,
        project,
        author,
        noteable.diff_head_commit
      )
    end

1038 1039 1040
    it_behaves_like 'a system note' do
      let(:action) { 'title' }
    end
1041 1042 1043 1044 1045 1046 1047

    it "posts the 'marked as a Work In Progress from commit' system note" do
      expect(subject.note).to match(
        /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
      )
    end
  end
1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058

  describe '.change_task_status' do
    let(:noteable) { create(:issue, project: project) }
    let(:task)     { double(:task, complete?: true, source: 'task') }

    subject { described_class.change_task_status(noteable, project, author, task) }

    it_behaves_like 'a system note' do
      let(:action) { 'task' }
    end

1059
    it "posts the 'marked the task as complete' system note" do
1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
      expect(subject.note).to eq("marked the task **task** as completed")
    end
  end

  describe '.resolve_all_discussions' do
    let(:noteable) { create(:merge_request, source_project: project, target_project: project) }

    subject { described_class.resolve_all_discussions(noteable, project, author) }

    it_behaves_like 'a system note' do
      let(:action) { 'discussion' }
    end

    it 'sets the note text' do
      expect(subject.note).to eq 'resolved all discussions'
    end
  end
1077 1078

  describe '.diff_discussion_outdated' do
1079
    let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
    let(:merge_request) { discussion.noteable }
    let(:change_position) { discussion.position }

    def reloaded_merge_request
      MergeRequest.find(merge_request.id)
    end

    subject { described_class.diff_discussion_outdated(discussion, project, author, change_position) }

    it_behaves_like 'a system note' do
      let(:expected_noteable) { discussion.first_note.noteable }
Douwe Maan's avatar
Douwe Maan committed
1091
      let(:action)            { 'outdated' }
1092 1093
    end

1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    context 'when the change_position is valid for the discussion' do
      it 'creates a new note in the discussion' do
        # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
        expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
      end

      it 'links to the diff in the system note' do
        expect(subject.note).to include('version 1')

        diff_id = merge_request.merge_request_diff.id
        line_code = change_position.line_code(project.repository)
        expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
      end
1107 1108
    end

1109 1110
    context 'when the change_position is invalid for the discussion' do
      let(:change_position) { project.commit(sample_commit.id) }
1111

1112 1113 1114 1115 1116 1117 1118 1119
      it 'creates a new note in the discussion' do
        # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
        expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
      end

      it 'does not create a link' do
        expect(subject.note).to eq('changed this line in version 1 of the diff')
      end
1120 1121
    end
  end
1122 1123

  describe '.mark_duplicate_issue' do
1124
    subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) }
1125 1126

    context 'within the same project' do
1127
      let(:canonical_issue) { create(:issue, project: project) }
1128