note_spec.rb 26.2 KB
Newer Older
gitlabhq's avatar
gitlabhq committed
1 2
require 'spec_helper'

3
describe Note do
4 5
  include RepoHelpers

6
  describe 'associations' do
7
    it { is_expected.to belong_to(:project) }
8
    it { is_expected.to belong_to(:noteable).touch(false) }
9
    it { is_expected.to belong_to(:author).class_name('User') }
10

11
    it { is_expected.to have_many(:todos) }
gitlabhq's avatar
gitlabhq committed
12 13
  end

14 15 16 17 18 19 20 21
  describe 'modules' do
    subject { described_class }

    it { is_expected.to include_module(Participable) }
    it { is_expected.to include_module(Mentionable) }
    it { is_expected.to include_module(Awardable) }
  end

22
  describe 'validation' do
23 24
    it { is_expected.to validate_presence_of(:note) }
    it { is_expected.to validate_presence_of(:project) }
25

26
    context 'when note is on commit' do
27 28 29
      before do
        allow(subject).to receive(:for_commit?).and_return(true)
      end
30 31

      it { is_expected.to validate_presence_of(:commit_id) }
Robert Speicher's avatar
Robert Speicher committed
32
      it { is_expected.not_to validate_presence_of(:noteable_id) }
33 34
    end

35
    context 'when note is not on commit' do
36 37 38
      before do
        allow(subject).to receive(:for_commit?).and_return(false)
      end
39

Robert Speicher's avatar
Robert Speicher committed
40
      it { is_expected.not_to validate_presence_of(:commit_id) }
41 42 43
      it { is_expected.to validate_presence_of(:noteable_id) }
    end

44
    context 'when noteable and note project differ' do
45
      subject do
46
        build(:note, noteable: build_stubbed(:issue),
47
                     project: build_stubbed(:project))
48 49 50 51 52
      end

      it { is_expected.to be_invalid }
    end

53
    context 'when noteable and note project are the same' do
54 55 56
      subject { create(:note) }
      it { is_expected.to be_valid }
    end
57 58 59 60 61 62 63

    context 'when project is missing for a project related note' do
      subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
      it { is_expected.to be_invalid }
    end

    context 'when noteable is a personal snippet' do
Jarka Kadlecova's avatar
Jarka Kadlecova committed
64
      subject { build(:note_on_personal_snippet) }
65 66 67 68 69

      it 'is valid without project' do
        is_expected.to be_valid
      end
    end
gitlabhq's avatar
gitlabhq committed
70 71
  end

72
  describe "Commit notes" do
Riyad Preukschas's avatar
Riyad Preukschas committed
73 74
    let!(:note) { create(:note_on_commit, note: "+1 from me") }
    let!(:commit) { note.noteable }
75

76
    it "is accessible through #noteable" do
77 78 79
      expect(note.commit_id).to eq(commit.id)
      expect(note.noteable).to be_a(Commit)
      expect(note.noteable).to eq(commit)
Riyad Preukschas's avatar
Riyad Preukschas committed
80 81
    end

82
    it "saves a valid note" do
83
      expect(note.commit_id).to eq(commit.id)
Riyad Preukschas's avatar
Riyad Preukschas committed
84
      note.noteable == commit
Riyad Preukschas's avatar
Riyad Preukschas committed
85 86
    end

87
    it "is recognized by #for_commit?" do
88
      expect(note).to be_for_commit
89
    end
90 91 92 93

    it "keeps the commit around" do
      expect(note.project.repository.kept_around?(commit.id)).to be_truthy
    end
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110

    it 'does not generate N+1 queries for participants', :request_store do
      def retrieve_participants
        commit.notes_with_associations.map(&:participants).to_a
      end

      # Project authorization checks are cached, establish a baseline
      retrieve_participants

      control_count = ActiveRecord::QueryRecorder.new do
        retrieve_participants
      end

      create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id)

      expect { retrieve_participants }.not_to exceed_query_limit(control_count)
    end
Riyad Preukschas's avatar
Riyad Preukschas committed
111 112
  end

113
  describe 'authorization' do
Nihad Abbasov's avatar
Nihad Abbasov committed
114
    before do
115 116
      @p1 = create(:project)
      @p2 = create(:project)
117 118 119
      @u1 = create(:user)
      @u2 = create(:user)
      @u3 = create(:user)
gitlabhq's avatar
gitlabhq committed
120 121
    end

122
    describe 'read' do
Nihad Abbasov's avatar
Nihad Abbasov committed
123
      before do
124 125
        @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST)
        @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
gitlabhq's avatar
gitlabhq committed
126 127
      end

128 129 130
      it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
      it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy }
      it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey }
gitlabhq's avatar
gitlabhq committed
131 132
    end

133
    describe 'write' do
Nihad Abbasov's avatar
Nihad Abbasov committed
134
      before do
135 136
        @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER)
        @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
gitlabhq's avatar
gitlabhq committed
137 138
      end

139 140 141
      it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
      it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy }
      it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey }
gitlabhq's avatar
gitlabhq committed
142 143
    end

144
    describe 'admin' do
Nihad Abbasov's avatar
Nihad Abbasov committed
145
      before do
146 147 148
        @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER)
        @p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER)
        @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER)
gitlabhq's avatar
gitlabhq committed
149 150
      end

151 152 153
      it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
      it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy }
      it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey }
gitlabhq's avatar
gitlabhq committed
154 155
    end
  end
156 157

  it_behaves_like 'an editable mentionable' do
Douwe Maan's avatar
Douwe Maan committed
158
    subject { create :note, noteable: issue, project: issue.project }
159

160
    let(:issue) { create(:issue, project: create(:project, :repository)) }
161 162 163
    let(:backref_text) { issue.gfm_reference }
    let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
  end
164

Douwe Maan's avatar
Douwe Maan committed
165
  describe "#all_references" do
166 167
    let!(:note1) { create(:note_on_issue) }
    let!(:note2) { create(:note_on_issue) }
Douwe Maan's avatar
Douwe Maan committed
168 169

    it "reads the rendered note body from the cache" do
170 171
      expect(Banzai::Renderer).to receive(:cache_collection_render)
        .with([{
172 173
          text: note1.note,
          context: {
174
            skip_project_check: false,
175 176 177 178 179 180 181
            pipeline: :note,
            cache_key: [note1, "note"],
            project: note1.project,
            author: note1.author
          }
        }]).and_call_original

182 183
      expect(Banzai::Renderer).to receive(:cache_collection_render)
        .with([{
184 185
          text: note2.note,
          context: {
186
            skip_project_check: false,
187 188 189 190 191 192 193 194 195
            pipeline: :note,
            cache_key: [note2, "note"],
            project: note2.project,
            author: note2.author
          }
        }]).and_call_original

      note1.all_references.users
      note2.all_references.users
Douwe Maan's avatar
Douwe Maan committed
196 197 198
    end
  end

199 200 201 202 203 204 205 206 207 208 209
  describe "editable?" do
    it "returns true" do
      note = build(:note)
      expect(note.editable?).to be_truthy
    end

    it "returns false" do
      note = build(:note, system: true)
      expect(note.editable?).to be_falsy
    end
  end
Douwe Maan's avatar
Douwe Maan committed
210

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
  describe "confidential?" do
    it "delegates to noteable" do
      issue_note = build(:note, :on_issue)
      confidential_note = build(:note, noteable: create(:issue, confidential: true))

      expect(issue_note.confidential?).to be_falsy
      expect(confidential_note.confidential?).to be_truthy
    end

    it "is falsey when noteable can't be confidential" do
      commit_note = build(:note_on_commit)
      expect(commit_note.confidential?).to be_falsy
    end
  end

226 227
  describe "cross_reference_not_visible_for?" do
    let(:private_user)    { create(:user) }
228
    let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } }
229 230
    let(:private_issue)   { create(:issue, project: private_project) }

231
    let(:ext_proj)  { create(:project, :public) }
232 233
    let(:ext_issue) { create(:issue, project: ext_proj) }

234
    let(:note) do
235 236
      create :note,
        noteable: ext_issue, project: ext_proj,
237
        note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
238
        system: true
239
    end
240 241 242 243 244 245 246 247

    it "returns true" do
      expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
    end

    it "returns false" do
      expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy
    end
248 249 250 251 252 253 254 255 256 257 258 259 260 261

    it "returns false if user visible reference count set" do
      note.user_visible_reference_count = 1

      expect(note).not_to receive(:reference_mentionables)
      expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_falsy
    end

    it "returns true if ref count is 0" do
      note.user_visible_reference_count = 0

      expect(note).not_to receive(:reference_mentionables)
      expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
    end
262 263
  end

micael.bergeron's avatar
micael.bergeron committed
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
  describe '#cross_reference?' do
    it 'falsey for user-generated notes' do
      note = create(:note, system: false)

      expect(note.cross_reference?).to be_falsy
    end

    context 'when the note might contain cross references' do
      SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type|
        let(:note) { create(:note, :system) }
        let!(:metadata) { create(:system_note_metadata, note: note, action: type) }

        it 'delegates to the cross-reference regex' do
          expect(note).to receive(:matches_cross_reference_regex?).and_return(false)

          note.cross_reference?
        end
      end
    end

    context 'when the note cannot contain cross references' do
      let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) }
      let(:label_note) { build(:note, note: 'added ~2323232323', system: true) }

      it 'scan for a `mentioned in` prefix' do
        expect(commit_note.cross_reference?).to be_truthy
        expect(label_note.cross_reference?).to be_falsy
      end
    end
  end

295 296 297 298 299 300 301
  describe 'clear_blank_line_code!' do
    it 'clears a blank line code before validation' do
      note = build(:note, line_code: ' ')

      expect { note.valid? }.to change(note, :line_code).to(nil)
    end
  end
Yorick Peterse's avatar
Yorick Peterse committed
302 303 304

  describe '#participants' do
    it 'includes the note author' do
305
      project = create(:project, :public)
Yorick Peterse's avatar
Yorick Peterse committed
306 307 308 309 310 311
      issue = create(:issue, project: project)
      note = create(:note_on_issue, noteable: issue, project: project)

      expect(note.participants).to include(note.author)
    end
  end
312

313
  describe '.find_discussion' do
Douwe Maan's avatar
Douwe Maan committed
314 315 316 317
    let!(:note) { create(:discussion_note_on_merge_request) }
    let!(:note2) { create(:discussion_note_on_merge_request, in_reply_to: note) }
    let(:merge_request) { note.noteable }

Douwe Maan's avatar
Douwe Maan committed
318
    it 'returns a discussion with multiple notes' do
Douwe Maan's avatar
Douwe Maan committed
319 320 321 322 323 324
      discussion = merge_request.notes.find_discussion(note.discussion_id)

      expect(discussion).not_to be_nil
      expect(discussion.notes).to match_array([note, note2])
      expect(discussion.first_note.discussion_id).to eq(note.discussion_id)
    end
325 326
  end

327 328 329 330
  describe ".grouped_diff_discussions" do
    let!(:merge_request) { create(:merge_request) }
    let(:project) { merge_request.project }
    let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
Douwe Maan's avatar
Douwe Maan committed
331
    let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: active_diff_note1) }
332 333
    let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
    let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
Douwe Maan's avatar
Douwe Maan committed
334
    let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: outdated_diff_note1) }
335 336 337 338 339

    let(:active_position2) do
      Gitlab::Diff::Position.new(
        old_path: "files/ruby/popen.rb",
        new_path: "files/ruby/popen.rb",
340 341 342
        old_line: nil,
        new_line: 13,
        diff_refs: project.commit(sample_commit.id).diff_refs
343 344 345 346 347 348 349 350 351 352 353 354 355
      )
    end

    let(:outdated_position) do
      Gitlab::Diff::Position.new(
        old_path: "files/ruby/popen.rb",
        new_path: "files/ruby/popen.rb",
        old_line: nil,
        new_line: 9,
        diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
      )
    end

356 357
    context 'active diff discussions' do
      subject { merge_request.notes.grouped_diff_discussions }
358

359 360
      it "includes active discussions" do
        discussions = subject.values.flatten
361

362 363 364
        expect(discussions.count).to eq(2)
        expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
        expect(discussions.all?(&:active?)).to be true
365

366 367 368
        expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
        expect(discussions.last.notes).to eq([active_diff_note3])
      end
369

370 371 372 373 374 375 376 377
      it "doesn't include outdated discussions" do
        expect(subject.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
      end

      it "groups the discussions by line code" do
        expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
        expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
      end
Felipe Artur's avatar
Felipe Artur committed
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

      context 'with image discussions' do
        let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images and changes") }
        let(:image_path) { "files/images/ee_repo_logo.png" }
        let(:text_path) { "bar/branch-test.txt" }
        let!(:image_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) }
        let!(:text_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: text_position) }

        let(:image_position) do
          Gitlab::Diff::Position.new(
            old_path: image_path,
            new_path: image_path,
            width: 100,
            height: 100,
            x: 1,
            y: 1,
            position_type: "image",
            diff_refs: merge_request2.diff_refs
          )
        end

        let(:text_position) do
          Gitlab::Diff::Position.new(
            old_path: text_path,
            new_path: text_path,
            old_line: nil,
            new_line: 2,
            position_type: "text",
            diff_refs: merge_request2.diff_refs
          )
        end

        it "groups image discussions by file identifier" do
          diff_discussion = DiffDiscussion.new([image_note])

          discussions = merge_request2.notes.grouped_diff_discussions

          expect(discussions.size).to eq(2)
          expect(discussions[image_note.diff_file.new_path]).to include(diff_discussion)
        end

        it "groups text discussions by line code" do
          diff_discussion = DiffDiscussion.new([text_note])

          discussions = merge_request2.notes.grouped_diff_discussions

          expect(discussions.size).to eq(2)
          expect(discussions[text_note.line_code]).to include(diff_discussion)
        end
      end
428 429
    end

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
    context 'diff discussions for older diff refs' do
      subject { merge_request.notes.grouped_diff_discussions(diff_refs) }

      context 'for diff refs a discussion was created at' do
        let(:diff_refs) { active_position2.diff_refs }

        it "includes discussions that were created then" do
          discussions = subject.values.flatten

          expect(discussions.count).to eq(1)

          discussion = discussions.first

          expect(discussion.id).to eq(active_diff_note3.discussion_id)
          expect(discussion.active?).to be true
          expect(discussion.active?(diff_refs)).to be false
          expect(discussion.created_at_diff?(diff_refs)).to be true

          expect(discussion.notes).to eq([active_diff_note3])
        end

        it "groups the discussions by original line code" do
          expect(subject[active_diff_note3.original_line_code].first.id).to eq(active_diff_note3.discussion_id)
        end
      end

      context 'for diff refs a discussion was last active at' do
        let(:diff_refs) { outdated_position.diff_refs }

        it "includes discussions that were last active" do
          discussions = subject.values.flatten

          expect(discussions.count).to eq(1)

          discussion = discussions.first

          expect(discussion.id).to eq(outdated_diff_note1.discussion_id)
          expect(discussion.active?).to be false
          expect(discussion.active?(diff_refs)).to be true
          expect(discussion.created_at_diff?(diff_refs)).to be true

          expect(discussion.notes).to eq([outdated_diff_note1, outdated_diff_note2])
        end

        it "groups the discussions by line code" do
          expect(subject[outdated_diff_note1.line_code].first.id).to eq(outdated_diff_note1.discussion_id)
        end
      end
478 479
    end
  end
480

Jarka Kadlecova's avatar
Jarka Kadlecova committed
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
  describe '#for_personal_snippet?' do
    it 'returns false for a project snippet note' do
      expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
    end

    it 'returns true for a personal snippet note' do
      expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
    end
  end

  describe '#to_ability_name' do
    it 'returns snippet for a project snippet note' do
      expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
    end

    it 'returns personal_snippet for a personal snippet note' do
      expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
    end

    it 'returns merge_request for an MR note' do
      expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
    end

    it 'returns issue for an issue note' do
      expect(build(:note_on_issue).to_ability_name).to eq('issue')
    end

    it 'returns issue for a commit note' do
      expect(build(:note_on_commit).to_ability_name).to eq('commit')
    end
  end

  describe '#cache_markdown_field' do
    let(:html) { '<p>some html</p>'}

    context 'note for a project snippet' do
      let(:note) { build(:note_on_project_snippet) }

      before do
520 521
        expect(Banzai::Renderer).to receive(:cacheless_render_field)
          .with(note, :note, { skip_project_check: false }).and_return(html)
Jarka Kadlecova's avatar
Jarka Kadlecova committed
522 523 524 525 526 527 528 529 530 531 532 533 534

        note.save
      end

      it 'creates a note' do
        expect(note.note_html).to eq(html)
      end
    end

    context 'note for a personal snippet' do
      let(:note) { build(:note_on_personal_snippet) }

      before do
535 536
        expect(Banzai::Renderer).to receive(:cacheless_render_field)
          .with(note, :note, { skip_project_check: true }).and_return(html)
Jarka Kadlecova's avatar
Jarka Kadlecova committed
537 538 539 540 541 542 543 544 545

        note.save
      end

      it 'creates a note' do
        expect(note.note_html).to eq(html)
      end
    end
  end
546

547
  describe '#can_be_discussion_note?' do
Douwe Maan's avatar
Douwe Maan committed
548 549
    context 'for a note on a merge request' do
      it 'returns true' do
Douwe Maan's avatar
Douwe Maan committed
550 551
        note = build(:note_on_merge_request)

Douwe Maan's avatar
Douwe Maan committed
552 553 554 555 556 557
        expect(note.can_be_discussion_note?).to be_truthy
      end
    end

    context 'for a note on an issue' do
      it 'returns true' do
Douwe Maan's avatar
Douwe Maan committed
558 559
        note = build(:note_on_issue)

Douwe Maan's avatar
Douwe Maan committed
560 561 562 563 564 565
        expect(note.can_be_discussion_note?).to be_truthy
      end
    end

    context 'for a note on a commit' do
      it 'returns true' do
Douwe Maan's avatar
Douwe Maan committed
566 567
        note = build(:note_on_commit)

Douwe Maan's avatar
Douwe Maan committed
568 569 570 571 572 573
        expect(note.can_be_discussion_note?).to be_truthy
      end
    end

    context 'for a note on a snippet' do
      it 'returns true' do
Douwe Maan's avatar
Douwe Maan committed
574 575
        note = build(:note_on_project_snippet)

Douwe Maan's avatar
Douwe Maan committed
576 577 578 579 580 581
        expect(note.can_be_discussion_note?).to be_truthy
      end
    end

    context 'for a diff note on merge request' do
      it 'returns false' do
Douwe Maan's avatar
Douwe Maan committed
582 583
        note = build(:diff_note_on_merge_request)

Douwe Maan's avatar
Douwe Maan committed
584 585 586 587 588 589
        expect(note.can_be_discussion_note?).to be_falsey
      end
    end

    context 'for a diff note on commit' do
      it 'returns false' do
Douwe Maan's avatar
Douwe Maan committed
590 591
        note = build(:diff_note_on_commit)

Douwe Maan's avatar
Douwe Maan committed
592 593 594 595 596 597
        expect(note.can_be_discussion_note?).to be_falsey
      end
    end

    context 'for a discussion note' do
      it 'returns false' do
Douwe Maan's avatar
Douwe Maan committed
598 599
        note = build(:discussion_note_on_merge_request)

Douwe Maan's avatar
Douwe Maan committed
600 601 602
        expect(note.can_be_discussion_note?).to be_falsey
      end
    end
603 604 605
  end

  describe '#discussion_class' do
Douwe Maan's avatar
Douwe Maan committed
606 607 608 609 610 611 612 613 614 615 616 617 618 619
    let(:note) { build(:note_on_commit) }
    let(:merge_request) { create(:merge_request) }

    context 'when the note is displayed out of context' do
      it 'returns OutOfContextDiscussion' do
        expect(note.discussion_class(merge_request)).to be(OutOfContextDiscussion)
      end
    end

    context 'when the note is displayed in the original context' do
      it 'returns IndividualNoteDiscussion' do
        expect(note.discussion_class(note.noteable)).to be(IndividualNoteDiscussion)
      end
    end
620 621 622
  end

  describe "#discussion_id" do
Douwe Maan's avatar
Douwe Maan committed
623
    let(:note) { create(:note_on_commit) }
624 625 626 627 628 629 630 631 632 633 634 635 636 637 638

    context "when it is newly created" do
      it "has a discussion id" do
        expect(note.discussion_id).not_to be_nil
        expect(note.discussion_id).to match(/\A\h{40}\z/)
      end
    end

    context "when it didn't store a discussion id before" do
      before do
        note.update_column(:discussion_id, nil)
      end

      it "has a discussion id" do
        # The discussion_id is set in `after_initialize`, so `reload` won't work
639
        reloaded_note = described_class.find(note.id)
640 641 642 643 644

        expect(reloaded_note.discussion_id).not_to be_nil
        expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
      end
    end
Douwe Maan's avatar
Douwe Maan committed
645 646 647 648 649 650 651 652

    context 'when the note is displayed out of context' do
      let(:merge_request) { create(:merge_request) }

      it 'overrides the discussion id' do
        expect(note.discussion_id(merge_request)).not_to eq(note.discussion_id)
      end
    end
653 654 655 656
  end

  describe '#to_discussion' do
    subject { create(:discussion_note_on_merge_request) }
Douwe Maan's avatar
Douwe Maan committed
657
    let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to: subject) }
658 659 660 661 662 663 664 665 666 667 668 669 670 671

    it "returns a discussion with just this note" do
      discussion = subject.to_discussion

      expect(discussion.id).to eq(subject.discussion_id)
      expect(discussion.notes).to eq([subject])
    end
  end

  describe "#discussion" do
    let!(:note1) { create(:discussion_note_on_merge_request) }
    let!(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }

    context 'when the note is part of a discussion' do
Douwe Maan's avatar
Douwe Maan committed
672
      subject { create(:discussion_note_on_merge_request, project: note1.project, noteable: note1.noteable, in_reply_to: note1) }
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694

      it "returns the discussion this note is in" do
        discussion = subject.discussion

        expect(discussion.id).to eq(subject.discussion_id)
        expect(discussion.notes).to eq([note1, subject])
      end
    end

    context 'when the note is not part of a discussion' do
      subject { create(:note) }

      it "returns a discussion with just this note" do
        discussion = subject.discussion

        expect(discussion.id).to eq(subject.discussion_id)
        expect(discussion.notes).to eq([subject])
      end
    end
  end

  describe "#part_of_discussion?" do
Douwe Maan's avatar
Douwe Maan committed
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
    context 'for a regular note' do
      let(:note) { build(:note) }

      it 'returns false' do
        expect(note.part_of_discussion?).to be_falsey
      end
    end

    context 'for a diff note' do
      let(:note) { build(:diff_note_on_commit) }

      it 'returns true' do
        expect(note.part_of_discussion?).to be_truthy
      end
    end

    context 'for a discussion note' do
      let(:note) { build(:discussion_note_on_merge_request) }

      it 'returns true' do
        expect(note.part_of_discussion?).to be_truthy
      end
    end
  end

  describe '#in_reply_to?' do
    context 'for a note' do
      context 'when part of a discussion' do
        subject { create(:discussion_note_on_issue) }
        let(:note) { create(:discussion_note_on_issue, in_reply_to: subject) }

        it 'checks if the note is in reply to the other discussion' do
          expect(subject).to receive(:in_reply_to?).with(note).and_call_original
          expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original
          expect(subject).to receive(:in_reply_to?).with(note.to_discussion).and_call_original

          subject.in_reply_to?(note)
        end
      end

      context 'when not part of a discussion' do
        subject { create(:note) }
        let(:note) { create(:note, in_reply_to: subject) }

        it 'checks if the note is in reply to the other noteable' do
          expect(subject).to receive(:in_reply_to?).with(note).and_call_original
          expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original

          subject.in_reply_to?(note)
        end
      end
    end

    context 'for a discussion' do
      context 'when part of the same discussion' do
        subject { create(:diff_note_on_merge_request) }
        let(:note) { create(:diff_note_on_merge_request, in_reply_to: subject) }

        it 'returns true' do
          expect(subject.in_reply_to?(note.to_discussion)).to be_truthy
        end
      end

      context 'when not part of the same discussion' do
        subject { create(:diff_note_on_merge_request) }
        let(:note) { create(:diff_note_on_merge_request) }

        it 'returns false' do
          expect(subject.in_reply_to?(note.to_discussion)).to be_falsey
        end
      end
    end

    context 'for a noteable' do
      context 'when a comment on the same noteable' do
        subject { create(:note) }
        let(:note) { create(:note, in_reply_to: subject) }

        it 'returns true' do
          expect(subject.in_reply_to?(note.noteable)).to be_truthy
        end
      end

      context 'when not a comment on the same noteable' do
        subject { create(:note) }
        let(:note) { create(:note) }

        it 'returns false' do
          expect(subject.in_reply_to?(note.noteable)).to be_falsey
        end
      end
    end
787 788
  end

789
  describe '#references' do
790
    context 'when part of a discussion' do
791 792 793 794 795
      it 'references all earlier notes in the discussion' do
        first_note = create(:discussion_note_on_issue)
        second_note = create(:discussion_note_on_issue, in_reply_to: first_note)
        third_note = create(:discussion_note_on_issue, in_reply_to: second_note)
        create(:discussion_note_on_issue, in_reply_to: third_note)
796

797
        expect(third_note.references).to eq([first_note.noteable, first_note, second_note])
798 799 800 801 802 803 804 805
      end
    end

    context 'when not part of a discussion' do
      subject { create(:note) }
      let(:note) { create(:note, in_reply_to: subject) }

      it 'returns the noteable' do
806
        expect(note.references).to eq([note.noteable])
807 808 809 810
      end
    end
  end

811 812 813
  describe 'expiring ETag cache' do
    let(:note) { build(:note_on_issue) }

814
    def expect_expiration(note)
815 816 817
      expect_any_instance_of(Gitlab::EtagCaching::Store)
        .to receive(:touch)
        .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
818 819 820 821
    end

    it "expires cache for note's issue when note is saved" do
      expect_expiration(note)
822 823 824

      note.save!
    end
825 826 827 828 829 830

    it "expires cache for note's issue when note is destroyed" do
      expect_expiration(note)

      note.destroy!
    end
831
  end
gitlabhq's avatar
gitlabhq committed
832
end