1# frozen_string_literal: true
2
3require 'spec_helper'
4
5RSpec.describe Projects::MergeRequests::DiffsController do
6  include ProjectForksHelper
7  include TrackingHelpers
8
9  shared_examples '404 for unexistent diffable' do
10    context 'when diffable does not exists' do
11      it 'returns 404' do
12        go(diff_id: non_existing_record_id)
13
14        expect(MergeRequestDiff.find_by(id: non_existing_record_id)).to be_nil
15        expect(response).to have_gitlab_http_status(:not_found)
16      end
17    end
18
19    context 'when the merge_request_diff.id is blank' do
20      it 'returns 404' do
21        allow_next_instance_of(MergeRequest) do |instance|
22          allow(instance).to receive(:merge_request_diff).and_return(MergeRequestDiff.new(merge_request_id: instance.id))
23
24          go
25
26          expect(response).to have_gitlab_http_status(:not_found)
27        end
28      end
29    end
30  end
31
32  shared_examples 'forked project with submodules' do
33    render_views
34
35    let(:project) { create(:project, :repository) }
36    let(:forked_project) { fork_project_with_submodules(project) }
37    let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
38
39    before do
40      project.add_developer(user)
41
42      merge_request.reload
43      go
44    end
45
46    it 'renders' do
47      expect(response).to be_successful
48      expect(response.body).to have_content('Subproject commit')
49    end
50  end
51
52  shared_examples 'cached diff collection' do
53    it 'ensures diff highlighting cache writing' do
54      expect_next_instance_of(Gitlab::Diff::HighlightCache) do |cache|
55        expect(cache).to receive(:write_if_empty).once
56      end
57
58      go
59    end
60  end
61
62  shared_examples "diff note on-demand position creation" do
63    it "updates diff discussion positions" do
64      service = double("service")
65
66      expect(Discussions::CaptureDiffNotePositionsService).to receive(:new).with(merge_request).and_return(service)
67      expect(service).to receive(:execute)
68
69      go
70    end
71  end
72
73  shared_examples 'show the right diff files with previous diff_id' do
74    context 'with previous diff_id' do
75      let!(:merge_request_diff_1) { merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
76      let!(:merge_request_diff_2) { merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e', diff_type: :merge_head) }
77
78      subject { go(diff_id: merge_request_diff_1.id, diff_head: true) }
79
80      it 'shows the right diff files' do
81        subject
82        expect(json_response["diff_files"].size).to eq(merge_request_diff_1.files_count)
83      end
84    end
85  end
86
87  let(:project) { create(:project, :repository) }
88  let(:user) { create(:user) }
89  let(:maintainer) { true }
90  let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
91
92  before do
93    project.add_maintainer(user) if maintainer
94    sign_in(user)
95  end
96
97  describe 'GET show' do
98    def go(extra_params = {})
99      params = {
100        namespace_id: project.namespace.to_param,
101        project_id: project,
102        id: merge_request.iid,
103        format: 'json'
104      }
105
106      get :show, params: params.merge(extra_params)
107    end
108
109    context 'with default params' do
110      context 'for the same project' do
111        before do
112          allow(controller).to receive(:rendered_for_merge_request?).and_return(true)
113        end
114
115        it 'serializes merge request diff collection' do
116          expect_next_instance_of(DiffsSerializer) do |instance|
117            expect(instance).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
118          end
119
120          go
121        end
122      end
123
124      context 'when note is a legacy diff note' do
125        before do
126          create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request)
127        end
128
129        it 'serializes merge request diff collection' do
130          expect_next_instance_of(DiffsSerializer) do |instance|
131            expect(instance).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
132          end
133
134          go
135        end
136      end
137
138      it_behaves_like 'forked project with submodules'
139    end
140
141    it_behaves_like 'cached diff collection'
142    it_behaves_like 'diff note on-demand position creation'
143  end
144
145  describe 'GET diffs_metadata' do
146    shared_examples_for 'serializes diffs metadata with expected arguments' do
147      it 'returns success' do
148        subject
149
150        expect(response).to have_gitlab_http_status(:ok)
151      end
152
153      it 'serializes paginated merge request diff collection' do
154        expect_next_instance_of(DiffsMetadataSerializer) do |instance|
155          expect(instance).to receive(:represent)
156            .with(an_instance_of(collection), expected_options)
157            .and_call_original
158        end
159
160        subject
161      end
162    end
163
164    def go(extra_params = {})
165      params = {
166        namespace_id: project.namespace.to_param,
167        project_id: project,
168        id: merge_request.iid,
169        format: 'json'
170      }
171
172      get :diffs_metadata, params: params.merge(extra_params)
173    end
174
175    it_behaves_like '404 for unexistent diffable'
176
177    it_behaves_like 'show the right diff files with previous diff_id'
178
179    context 'when not authorized' do
180      let(:another_user) { create(:user) }
181
182      before do
183        sign_in(another_user)
184      end
185
186      it 'returns 404 when not a member' do
187        go
188
189        expect(response).to have_gitlab_http_status(:not_found)
190      end
191
192      it 'returns 404 when visibility level is not enough' do
193        project.add_guest(another_user)
194
195        go
196
197        expect(response).to have_gitlab_http_status(:not_found)
198      end
199    end
200
201    context 'with valid diff_id' do
202      subject { go(diff_id: merge_request.merge_request_diff.id) }
203
204      it_behaves_like 'serializes diffs metadata with expected arguments' do
205        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
206        let(:expected_options) do
207          {
208            environment: nil,
209            merge_request: merge_request,
210            merge_request_diff: merge_request.merge_request_diff,
211            merge_request_diffs: merge_request.merge_request_diffs,
212            start_version: nil,
213            start_sha: nil,
214            commit: nil,
215            latest_diff: true,
216            only_context_commits: false,
217            allow_tree_conflicts: true,
218            merge_ref_head_diff: false
219          }
220        end
221      end
222    end
223
224    context "with the :default_merge_ref_for_diffs flag on" do
225      let(:diffable_merge_ref) { true }
226
227      subject do
228        go(diff_head: true,
229           diff_id: merge_request.merge_request_diff.id,
230           start_sha: merge_request.merge_request_diff.start_commit_sha)
231      end
232
233      it "correctly generates the right diff between versions" do
234        MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author).execute(merge_request)
235
236        expect_next_instance_of(CompareService) do |service|
237          expect(service).to receive(:execute).with(
238            project,
239            merge_request.merge_request_diff.head_commit_sha,
240            straight: true)
241        end
242
243        subject
244      end
245    end
246
247    context 'with diff_head param passed' do
248      before do
249        allow(merge_request).to receive(:diffable_merge_ref?)
250          .and_return(diffable_merge_ref)
251      end
252
253      context 'the merge request can be compared with head' do
254        let(:diffable_merge_ref) { true }
255
256        it 'compares diffs with the head' do
257          create(:merge_request_diff, :merge_head, merge_request: merge_request)
258
259          go(diff_head: true)
260
261          expect(response).to have_gitlab_http_status(:ok)
262        end
263      end
264
265      context 'the merge request cannot be compared with head' do
266        let(:diffable_merge_ref) { false }
267
268        it 'compares diffs with the base' do
269          go(diff_head: true)
270
271          expect(response).to have_gitlab_http_status(:ok)
272        end
273      end
274    end
275
276    context 'with MR regular diff params' do
277      subject { go }
278
279      it_behaves_like 'serializes diffs metadata with expected arguments' do
280        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
281        let(:expected_options) do
282          {
283            environment: nil,
284            merge_request: merge_request,
285            merge_request_diff: merge_request.merge_request_diff,
286            merge_request_diffs: merge_request.merge_request_diffs,
287            start_version: nil,
288            start_sha: nil,
289            commit: nil,
290            latest_diff: true,
291            only_context_commits: false,
292            allow_tree_conflicts: true,
293            merge_ref_head_diff: nil
294          }
295        end
296      end
297    end
298
299    context 'with commit param' do
300      subject { go(commit_id: merge_request.diff_head_sha) }
301
302      it_behaves_like 'serializes diffs metadata with expected arguments' do
303        let(:collection) { Gitlab::Diff::FileCollection::Commit }
304        let(:expected_options) do
305          {
306            environment: nil,
307            merge_request: merge_request,
308            merge_request_diff: nil,
309            merge_request_diffs: merge_request.merge_request_diffs,
310            start_version: nil,
311            start_sha: nil,
312            commit: merge_request.diff_head_commit,
313            latest_diff: nil,
314            only_context_commits: false,
315            allow_tree_conflicts: true,
316            merge_ref_head_diff: nil
317          }
318        end
319      end
320    end
321
322    context 'when display_merge_conflicts_in_diff is disabled' do
323      subject { go }
324
325      before do
326        stub_feature_flags(display_merge_conflicts_in_diff: false)
327      end
328
329      it_behaves_like 'serializes diffs metadata with expected arguments' do
330        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
331        let(:expected_options) do
332          {
333            environment: nil,
334            merge_request: merge_request,
335            merge_request_diff: merge_request.merge_request_diff,
336            merge_request_diffs: merge_request.merge_request_diffs,
337            start_version: nil,
338            start_sha: nil,
339            commit: nil,
340            latest_diff: true,
341            only_context_commits: false,
342            allow_tree_conflicts: false,
343            merge_ref_head_diff: nil
344          }
345        end
346      end
347    end
348  end
349
350  describe 'GET diff_for_path' do
351    def diff_for_path(extra_params = {})
352      params = {
353        namespace_id: project.namespace.to_param,
354        project_id: project,
355        id: merge_request.iid,
356        format: 'json'
357      }
358
359      get :diff_for_path, params: params.merge(extra_params)
360    end
361
362    let(:existing_path) { 'files/ruby/popen.rb' }
363
364    context 'when the merge request exists' do
365      context 'when the user can view the merge request' do
366        context 'when the path exists in the diff' do
367          it 'enables diff notes' do
368            diff_for_path(old_path: existing_path, new_path: existing_path)
369
370            expect(assigns(:diff_notes_disabled)).to be_falsey
371            expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest',
372                                                        noteable_id: merge_request.id,
373                                                        commit_id: nil)
374          end
375
376          it 'only renders the diffs for the path given' do
377            diff_for_path(old_path: existing_path, new_path: existing_path)
378
379            paths = json_response['diff_files'].map { |file| file['new_path'] }
380
381            expect(paths).to include(existing_path)
382          end
383        end
384      end
385
386      context 'when the user cannot view the merge request' do
387        let(:maintainer) { false }
388
389        before do
390          diff_for_path(old_path: existing_path, new_path: existing_path)
391        end
392
393        it 'returns a 404' do
394          expect(response).to have_gitlab_http_status(:not_found)
395        end
396      end
397    end
398
399    context 'when the merge request does not exist' do
400      before do
401        diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path)
402      end
403
404      it 'returns a 404' do
405        expect(response).to have_gitlab_http_status(:not_found)
406      end
407    end
408
409    context 'when the merge request belongs to a different project' do
410      let(:other_project) { create(:project) }
411
412      before do
413        other_project.add_maintainer(user)
414        diff_for_path(old_path: existing_path, new_path: existing_path, project_id: other_project)
415      end
416
417      it 'returns a 404' do
418        expect(response).to have_gitlab_http_status(:not_found)
419      end
420    end
421  end
422
423  describe 'GET diffs_batch' do
424    shared_examples_for 'serializes diffs with expected arguments' do
425      it 'serializes paginated merge request diff collection' do
426        expect_next_instance_of(PaginatedDiffSerializer) do |instance|
427          expect(instance).to receive(:represent)
428            .with(an_instance_of(collection), expected_options)
429            .and_call_original
430        end
431
432        subject
433      end
434    end
435
436    shared_examples_for 'successful request' do
437      it 'returns success' do
438        subject
439
440        expect(response).to have_gitlab_http_status(:ok)
441      end
442
443      it 'tracks mr_diffs event' do
444        expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
445          .to receive(:track_mr_diffs_action)
446          .with(merge_request: merge_request)
447
448        subject
449      end
450
451      context 'when DNT is enabled' do
452        before do
453          stub_do_not_track('1')
454        end
455
456        it 'does not track any mr_diffs event' do
457          expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
458            .not_to receive(:track_mr_diffs_action)
459
460          expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
461            .not_to receive(:track_mr_diffs_single_file_action)
462
463          subject
464        end
465      end
466
467      context 'when user has view_diffs_file_by_file set to false' do
468        before do
469          user.update!(view_diffs_file_by_file: false)
470        end
471
472        it 'does not track single_file_diffs events' do
473          expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
474            .not_to receive(:track_mr_diffs_single_file_action)
475
476          subject
477        end
478      end
479
480      context 'when user has view_diffs_file_by_file set to true' do
481        before do
482          user.update!(view_diffs_file_by_file: true)
483        end
484
485        it 'tracks single_file_diffs events' do
486          expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
487            .to receive(:track_mr_diffs_single_file_action)
488            .with(merge_request: merge_request, user: user)
489
490          subject
491        end
492      end
493    end
494
495    def collection_arguments(pagination_data = {})
496      {
497        environment: nil,
498        merge_request: merge_request,
499        commit: nil,
500        diff_view: :inline,
501        merge_ref_head_diff: nil,
502        allow_tree_conflicts: true,
503        pagination_data: {
504          total_pages: nil
505        }.merge(pagination_data)
506      }
507    end
508
509    def go(extra_params = {})
510      params = {
511        namespace_id: project.namespace.to_param,
512        project_id: project,
513        id: merge_request.iid,
514        page: 0,
515        per_page: 20,
516        format: 'json'
517      }
518
519      get :diffs_batch, params: params.merge(extra_params)
520    end
521
522    it_behaves_like '404 for unexistent diffable'
523
524    it_behaves_like 'show the right diff files with previous diff_id'
525
526    context 'when not authorized' do
527      let(:other_user) { create(:user) }
528
529      before do
530        sign_in(other_user)
531      end
532
533      it 'returns 404' do
534        go
535
536        expect(response).to have_gitlab_http_status(:not_found)
537      end
538    end
539
540    context 'with valid diff_id' do
541      subject { go(diff_id: merge_request.merge_request_diff.id) }
542
543      it_behaves_like 'serializes diffs with expected arguments' do
544        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
545        let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: false) }
546      end
547
548      it_behaves_like 'successful request'
549    end
550
551    context 'with commit_id param' do
552      subject { go(commit_id: merge_request.diff_head_sha) }
553
554      it_behaves_like 'serializes diffs with expected arguments' do
555        let(:collection) { Gitlab::Diff::FileCollection::Commit }
556        let(:expected_options) { collection_arguments.merge(commit: merge_request.commits(limit: 1).first) }
557      end
558    end
559
560    context 'with diff_id and start_sha params' do
561      subject do
562        go(diff_id: merge_request.merge_request_diff.id,
563           start_sha: merge_request.merge_request_diff.start_commit_sha)
564      end
565
566      it_behaves_like 'serializes diffs with expected arguments' do
567        let(:collection) { Gitlab::Diff::FileCollection::Compare }
568        let(:expected_options) { collection_arguments.merge(merge_ref_head_diff: false) }
569      end
570
571      it_behaves_like 'successful request'
572    end
573
574    context 'with paths param' do
575      let(:example_file_path) { "README" }
576      let(:file_path_option) { { paths: [example_file_path] } }
577
578      subject do
579        go(file_path_option)
580      end
581
582      it_behaves_like 'serializes diffs with expected arguments' do
583        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
584        let(:expected_options) do
585          collection_arguments(total_pages: 20)
586        end
587      end
588
589      it_behaves_like 'successful request'
590
591      it 'filters down the response to the expected file path' do
592        subject
593
594        expect(json_response["diff_files"].size).to eq(1)
595        expect(json_response["diff_files"].first["file_path"]).to eq(example_file_path)
596      end
597    end
598
599    context 'with default params' do
600      subject { go }
601
602      it_behaves_like 'serializes diffs with expected arguments' do
603        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
604        let(:expected_options) { collection_arguments(total_pages: 20) }
605      end
606
607      it_behaves_like 'successful request'
608    end
609
610    context 'with smaller diff batch params' do
611      subject { go(page: 5, per_page: 5) }
612
613      it_behaves_like 'serializes diffs with expected arguments' do
614        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
615        let(:expected_options) { collection_arguments(total_pages: 20) }
616      end
617
618      it_behaves_like 'successful request'
619    end
620
621    context 'when display_merge_conflicts_in_diff is disabled' do
622      before do
623        stub_feature_flags(display_merge_conflicts_in_diff: false)
624      end
625
626      subject { go }
627
628      it_behaves_like 'serializes diffs with expected arguments' do
629        let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
630        let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) }
631      end
632
633      it_behaves_like 'successful request'
634    end
635
636    it_behaves_like 'forked project with submodules'
637    it_behaves_like 'cached diff collection'
638
639    context 'diff unfolding' do
640      let!(:unfoldable_diff_note) do
641        create(:diff_note_on_merge_request, :folded_position, project: project, noteable: merge_request)
642      end
643
644      let!(:diff_note) do
645        create(:diff_note_on_merge_request, project: project, noteable: merge_request)
646      end
647
648      it 'unfolds correct diff file positions' do
649        expect_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch) do |instance|
650          expect(instance)
651            .to receive(:unfold_diff_files)
652            .with([unfoldable_diff_note.position])
653            .and_call_original
654        end
655
656        go
657      end
658    end
659  end
660end
661