1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe DiffNote do 6 include RepoHelpers 7 8 let_it_be(:merge_request) { create(:merge_request) } 9 let_it_be(:project) { merge_request.project } 10 let_it_be(:commit) { project.commit(sample_commit.id) } 11 12 let_it_be(:path) { "files/ruby/popen.rb" } 13 14 let(:diff_refs) { merge_request.diff_refs } 15 let!(:position) do 16 Gitlab::Diff::Position.new( 17 old_path: path, 18 new_path: path, 19 old_line: nil, 20 new_line: 14, 21 diff_refs: diff_refs 22 ) 23 end 24 25 let!(:new_position) do 26 Gitlab::Diff::Position.new( 27 old_path: path, 28 new_path: path, 29 old_line: 16, 30 new_line: 22, 31 diff_refs: diff_refs 32 ) 33 end 34 35 subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } 36 37 describe 'validations' do 38 it_behaves_like 'a valid diff positionable note' do 39 subject { build(:diff_note_on_commit, project: project, commit_id: commit_id, position: position) } 40 end 41 42 it "is not valid when noteable is empty" do 43 note = build(:diff_note_on_merge_request, project: project, noteable: nil) 44 45 note.valid? 46 47 expect(note.errors[:noteable]).to include("doesn't support new-style diff notes") 48 end 49 50 context 'when importing' do 51 it "does not check if it's supported" do 52 note = build(:diff_note_on_merge_request, project: project, noteable: nil) 53 note.importing = true 54 note.valid? 55 56 expect(note.errors.full_messages).not_to include( 57 "Noteable doesn't support new-style diff notes" 58 ) 59 end 60 end 61 end 62 63 describe "#position=" do 64 context "when provided a string" do 65 it "sets the position" do 66 subject.position = new_position.to_json 67 68 expect(subject.position).to eq(new_position) 69 end 70 end 71 72 context "when provided a hash" do 73 it "sets the position" do 74 subject.position = new_position.to_h 75 76 expect(subject.position).to eq(new_position) 77 end 78 end 79 80 context "when provided a position object" do 81 it "sets the position" do 82 subject.position = new_position 83 84 expect(subject.position).to eq(new_position) 85 end 86 end 87 end 88 89 describe "#original_position=" do 90 context "when provided a string" do 91 it "sets the original position" do 92 subject.original_position = new_position.to_json 93 94 expect(subject.original_position).to eq(new_position) 95 end 96 end 97 98 context "when provided a hash" do 99 it "sets the original position" do 100 subject.original_position = new_position.to_h 101 102 expect(subject.original_position).to eq(new_position) 103 end 104 end 105 106 context "when provided a position object" do 107 it "sets the original position" do 108 subject.original_position = new_position 109 110 expect(subject.original_position).to eq(new_position) 111 end 112 end 113 end 114 115 describe '#create_diff_file callback' do 116 context 'merge request' do 117 let(:position) do 118 Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", 119 new_path: "files/ruby/popen.rb", 120 old_line: nil, 121 new_line: 9, 122 diff_refs: merge_request.diff_refs) 123 end 124 125 subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } 126 127 let(:diff_file_from_repository) do 128 position.diff_file(project.repository) 129 end 130 131 let(:diff_file) do 132 diffs = merge_request.diffs 133 raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['files/ruby/popen.rb'])).first 134 Gitlab::Diff::File.new(raw_diff, 135 repository: diffs.project.repository, 136 diff_refs: diffs.diff_refs, 137 fallback_diff_refs: diffs.fallback_diff_refs) 138 end 139 140 let(:diff_line) { diff_file.diff_lines.first } 141 142 let(:line_code) { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14' } 143 144 before do 145 allow(subject.position).to receive(:line_code).and_return('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14') 146 end 147 148 context 'when diffs are already created' do 149 before do 150 allow(subject).to receive(:created_at_diff?).and_return(true) 151 end 152 153 context 'when diff_file is found in persisted diffs' do 154 before do 155 allow(merge_request).to receive_message_chain(:diffs, :diff_files, :first).and_return(diff_file) 156 end 157 158 context 'when importing' do 159 before do 160 subject.importing = true 161 subject.line_code = line_code 162 end 163 164 context 'when diff_line is found in persisted diff_file' do 165 before do 166 allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line) 167 end 168 169 it 'creates a diff note file' do 170 subject.save! 171 expect(subject.note_diff_file).to be_present 172 end 173 end 174 175 context 'when diff_line is not found in persisted diff_file' do 176 before do 177 allow(diff_file).to receive(:line_for_position).and_return(nil) 178 end 179 180 it_behaves_like 'a valid diff note with after commit callback' 181 end 182 end 183 184 context 'when not importing' do 185 context 'when diff_line is not found' do 186 before do 187 allow(diff_file).to receive(:line_for_position).with(position).and_return(nil) 188 end 189 190 it 'raises an error' do 191 expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError, 192 "Failed to find diff line for: #{diff_file.file_path}, "\ 193 "old_line: #{position.old_line}"\ 194 ", new_line: #{position.new_line}") 195 end 196 end 197 198 context 'when diff_line is found' do 199 before do 200 allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line) 201 end 202 203 it 'creates a diff note file' do 204 subject.save! 205 expect(subject.reload.note_diff_file).to be_present 206 end 207 end 208 end 209 end 210 211 context 'when diff file is not found in persisted diffs' do 212 before do 213 allow_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff) do |merge_request_diff| 214 allow(merge_request_diff).to receive(:diff_files).and_return([]) 215 end 216 end 217 218 it_behaves_like 'a valid diff note with after commit callback' 219 end 220 end 221 222 context 'when diffs are not already created' do 223 before do 224 allow(subject).to receive(:created_at_diff?).and_return(false) 225 end 226 227 it_behaves_like 'a valid diff note with after commit callback' 228 end 229 230 it 'does not create diff note file if it is a reply' do 231 diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request) 232 233 expect { create(:diff_note_on_merge_request, noteable: merge_request, in_reply_to: diff_note) } 234 .not_to change(NoteDiffFile, :count) 235 end 236 end 237 238 context 'commit' do 239 let!(:diff_note) { create(:diff_note_on_commit, project: project) } 240 241 it 'creates a diff note file' do 242 expect(diff_note.reload.note_diff_file).to be_present 243 end 244 245 it 'does not create diff note file if it is a reply' do 246 expect { create(:diff_note_on_commit, in_reply_to: diff_note) } 247 .not_to change(NoteDiffFile, :count) 248 end 249 end 250 end 251 252 describe '#diff_file', :clean_gitlab_redis_shared_state do 253 context 'when note_diff_file association exists' do 254 it 'returns persisted diff file data' do 255 diff_file = subject.diff_file 256 257 expect(diff_file.diff.to_hash.with_indifferent_access) 258 .to include(subject.note_diff_file.to_hash) 259 end 260 end 261 262 context 'when the discussion was created in the diff' do 263 context 'when file_identifier_hash is disabled' do 264 before do 265 stub_feature_flags(file_identifier_hash: false) 266 end 267 268 it 'returns correct diff file' do 269 diff_file = subject.diff_file 270 271 expect(diff_file.old_path).to eq(position.old_path) 272 expect(diff_file.new_path).to eq(position.new_path) 273 expect(diff_file.diff_refs).to eq(position.diff_refs) 274 end 275 end 276 277 context 'when file_identifier_hash is enabled' do 278 before do 279 stub_feature_flags(file_identifier_hash: true) 280 end 281 282 it 'returns correct diff file' do 283 diff_file = subject.diff_file 284 285 expect(diff_file.old_path).to eq(position.old_path) 286 expect(diff_file.new_path).to eq(position.new_path) 287 expect(diff_file.diff_refs).to eq(position.diff_refs) 288 end 289 end 290 end 291 292 context 'when discussion is outdated or not created in the diff' do 293 let(:diff_refs) { project.commit(sample_commit.id).diff_refs } 294 let(:position) do 295 Gitlab::Diff::Position.new( 296 old_path: "files/ruby/popen.rb", 297 new_path: "files/ruby/popen.rb", 298 old_line: nil, 299 new_line: 14, 300 diff_refs: diff_refs 301 ) 302 end 303 304 it 'returns the correct diff file' do 305 diff_file = subject.diff_file 306 307 expect(diff_file.old_path).to eq(position.old_path) 308 expect(diff_file.new_path).to eq(position.new_path) 309 expect(diff_file.diff_refs).to eq(position.diff_refs) 310 end 311 end 312 313 context 'note diff file creation enqueuing' do 314 it 'enqueues CreateNoteDiffFileWorker if it is the first note of a discussion' do 315 subject.note_diff_file.destroy! 316 317 expect(CreateNoteDiffFileWorker).to receive(:perform_async).with(subject.id) 318 319 subject.reload.diff_file 320 end 321 322 it 'does not enqueues CreateNoteDiffFileWorker if not first note of a discussion' do 323 mr = create(:merge_request) 324 diff_note = create(:diff_note_on_merge_request, project: mr.project, noteable: mr) 325 reply_diff_note = create(:diff_note_on_merge_request, in_reply_to: diff_note) 326 327 expect(CreateNoteDiffFileWorker).not_to receive(:perform_async).with(reply_diff_note.id) 328 329 reply_diff_note.reload.diff_file 330 end 331 end 332 333 context 'when noteable is a Design' do 334 it 'does not return a diff file' do 335 diff_note = create(:diff_note_on_design) 336 337 expect(diff_note.diff_file).to be_nil 338 end 339 end 340 end 341 342 describe '#latest_diff_file' do 343 context 'when noteable is a Design' do 344 it 'does not return a diff file' do 345 diff_note = create(:diff_note_on_design) 346 347 expect(diff_note.latest_diff_file).to be_nil 348 end 349 end 350 end 351 352 describe "#diff_line" do 353 it "returns the correct diff line" do 354 diff_line = subject.diff_line 355 356 expect(diff_line.added?).to be true 357 expect(diff_line.new_line).to eq(position.formatter.new_line) 358 expect(diff_line.text).to eq("+ vars = {") 359 end 360 end 361 362 describe "#line_code" do 363 it "returns the correct line code" do 364 line_code = Gitlab::Git.diff_line_code(position.file_path, position.formatter.new_line, 15) 365 366 expect(subject.line_code).to eq(line_code) 367 end 368 end 369 370 describe "#active?" do 371 context "when noteable is a commit" do 372 subject { build(:diff_note_on_commit, project: project, position: position) } 373 374 it "returns true" do 375 expect(subject.active?).to be true 376 end 377 end 378 379 context "when noteable is a merge request" do 380 context "when the merge request's diff refs match that of the diff note" do 381 it "returns true" do 382 expect(subject.active?).to be true 383 end 384 end 385 386 context "when the merge request's diff refs don't match that of the diff note" do 387 before do 388 allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) 389 end 390 391 it "returns false" do 392 expect(subject.active?).to be false 393 end 394 end 395 end 396 end 397 398 describe "creation" do 399 describe "updating of position" do 400 context "when noteable is a commit" do 401 let(:diff_refs) { commit.diff_refs } 402 403 subject { create(:diff_note_on_commit, project: project, position: position, commit_id: commit.id) } 404 405 it "doesn't update the position" do 406 is_expected.to have_attributes(original_position: position, 407 position: position) 408 end 409 end 410 411 context "when noteable is a merge request" do 412 context "when the note is active" do 413 it "doesn't update the position" do 414 expect(subject.original_position).to eq(position) 415 expect(subject.position).to eq(position) 416 end 417 end 418 419 context "when the note is outdated" do 420 before do 421 allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) 422 end 423 424 it "updates the position" do 425 expect(subject.original_position).to eq(position) 426 expect(subject.position).not_to eq(position) 427 end 428 end 429 end 430 end 431 end 432 433 describe "#discussion_id" do 434 let(:note) { create(:diff_note_on_merge_request) } 435 436 context "when it is newly created" do 437 it "has a discussion id" do 438 expect(note.discussion_id).not_to be_nil 439 expect(note.discussion_id).to match(/\A\h{40}\z/) 440 end 441 end 442 443 context "when it didn't store a discussion id before" do 444 before do 445 note.update_column(:discussion_id, nil) 446 end 447 448 it "has a discussion id" do 449 # The discussion_id is set in `after_initialize`, so `reload` won't work 450 reloaded_note = Note.find(note.id) 451 452 expect(reloaded_note.discussion_id).not_to be_nil 453 expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/) 454 end 455 end 456 end 457 458 describe '#created_at_diff?' do 459 let(:diff_refs) { project.commit(sample_commit.id).diff_refs } 460 let(:position) do 461 Gitlab::Diff::Position.new( 462 old_path: "files/ruby/popen.rb", 463 new_path: "files/ruby/popen.rb", 464 old_line: nil, 465 new_line: 14, 466 diff_refs: diff_refs 467 ) 468 end 469 470 context "when noteable is a commit" do 471 subject { build(:diff_note_on_commit, project: project, position: position) } 472 473 it "returns true" do 474 expect(subject.created_at_diff?(diff_refs)).to be true 475 end 476 end 477 478 context "when noteable is a merge request" do 479 context "when the diff refs match the original one of the diff note" do 480 it "returns true" do 481 expect(subject.created_at_diff?(diff_refs)).to be true 482 end 483 end 484 485 context "when the diff refs don't match the original one of the diff note" do 486 it "returns false" do 487 expect(subject.created_at_diff?(merge_request.diff_refs)).to be false 488 end 489 end 490 end 491 end 492 493 describe '#supports_suggestion?' do 494 context 'when noteable does not exist' do 495 it 'returns false' do 496 allow(subject).to receive(:noteable) { nil } 497 498 expect(subject.supports_suggestion?).to be(false) 499 end 500 end 501 502 context 'when noteable does not support suggestions' do 503 it 'returns false' do 504 allow(subject.noteable).to receive(:supports_suggestion?) { false } 505 506 expect(subject.supports_suggestion?).to be(false) 507 end 508 end 509 510 context 'when line is not suggestible' do 511 it 'returns false' do 512 allow_next_instance_of(Gitlab::Diff::Line) do |instance| 513 allow(instance).to receive(:suggestible?) { false } 514 end 515 516 expect(subject.supports_suggestion?).to be(false) 517 end 518 end 519 end 520 521 describe '#banzai_render_context' do 522 let(:note) { create(:diff_note_on_merge_request) } 523 524 it 'includes expected context' do 525 context = note.banzai_render_context(:note) 526 527 expect(context).to include(suggestions_filter_enabled: true, noteable: note.noteable, project: note.project) 528 end 529 end 530 531 describe "image diff notes" do 532 subject { build(:image_diff_note_on_merge_request, project: project, noteable: merge_request) } 533 534 describe "validations" do 535 it { is_expected.not_to validate_presence_of(:line_code) } 536 537 it "does not validate diff line" do 538 diff_line = subject.diff_line 539 540 expect(diff_line).to be nil 541 expect(subject).to be_valid 542 end 543 544 it "does not update the position" do 545 expect(subject).not_to receive(:update_position) 546 547 subject.save! 548 end 549 end 550 551 it "returns true for on_image?" do 552 expect(subject.on_image?).to be_truthy 553 end 554 end 555 556 describe '#to_ability_name' do 557 subject { described_class.new.to_ability_name } 558 559 it { is_expected.to eq('note') } 560 end 561 562 describe '#shas' do 563 it 'returns list of SHAs based on original_position' do 564 expect(subject.shas).to match_array([ 565 position.base_sha, 566 position.start_sha, 567 position.head_sha 568 ]) 569 end 570 571 context 'when position changes' do 572 before do 573 subject.position = new_position 574 end 575 576 it 'includes the new position SHAs' do 577 expect(subject.shas).to match_array([ 578 position.base_sha, 579 position.start_sha, 580 position.head_sha, 581 new_position.base_sha, 582 new_position.start_sha, 583 new_position.head_sha 584 ]) 585 end 586 end 587 end 588end 589