1# frozen_string_literal: true
2
3require 'spec_helper'
4
5RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
6  include FilterSpecHelper
7
8  let(:project) { create(:project, :public) }
9  let(:merge)   { create(:merge_request, source_project: project) }
10
11  it 'requires project context' do
12    expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
13  end
14
15  %w(pre code a style).each do |elem|
16    it "ignores valid references contained inside '#{elem}' element" do
17      exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
18      expect(reference_filter(act).to_html).to eq exp
19    end
20  end
21
22  describe 'performance' do
23    let(:another_merge) { create(:merge_request, source_project: project, source_branch: 'fix') }
24
25    it 'does not have a N+1 query problem' do
26      single_reference = "Merge request #{merge.to_reference}"
27      multiple_references = "Merge requests #{merge.to_reference} and #{another_merge.to_reference}"
28
29      control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
30
31      expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
32    end
33  end
34
35  describe 'all references' do
36    let(:doc) { reference_filter(merge.to_reference) }
37    let(:tag_el) { doc.css('a').first }
38
39    it 'adds merge request iid' do
40      expect(tag_el["data-iid"]).to eq(merge.iid.to_s)
41    end
42
43    it 'adds project data attribute with project id' do
44      expect(tag_el["data-project-path"]).to eq(project.full_path)
45    end
46
47    it 'does not add `has-tooltip` class' do
48      expect(tag_el["class"]).not_to include('has-tooltip')
49    end
50  end
51
52  context 'internal reference' do
53    let(:reference) { merge.to_reference }
54
55    it 'links to a valid reference' do
56      doc = reference_filter("See #{reference}")
57
58      expect(doc.css('a').first.attr('href')).to eq urls
59        .project_merge_request_url(project, merge)
60    end
61
62    it 'links with adjacent text' do
63      doc = reference_filter("Merge (#{reference}.)")
64      expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
65    end
66
67    it 'ignores invalid merge IDs' do
68      exp = act = "Merge #{invalidate_reference(reference)}"
69
70      expect(reference_filter(act).to_html).to eq exp
71    end
72
73    it 'ignores out-of-bounds merge request IDs on the referenced project' do
74      exp = act = "Merge !#{Gitlab::Database::MAX_INT_VALUE + 1}"
75
76      expect(reference_filter(act).to_html).to eq exp
77    end
78
79    it 'has no title' do
80      doc = reference_filter("Merge #{reference}")
81      expect(doc.css('a').first.attr('title')).to eq ""
82    end
83
84    it 'escapes the title attribute' do
85      merge.update_attribute(:title, %{"></a>whatever<a title="})
86
87      doc = reference_filter("Merge #{reference}")
88      expect(doc.text).to eq "Merge #{reference}"
89    end
90
91    it 'includes default classes, without tooltip' do
92      doc = reference_filter("Merge #{reference}")
93      expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
94    end
95
96    it 'includes a data-project attribute' do
97      doc = reference_filter("Merge #{reference}")
98      link = doc.css('a').first
99
100      expect(link).to have_attribute('data-project')
101      expect(link.attr('data-project')).to eq project.id.to_s
102    end
103
104    it 'includes a data-merge-request attribute' do
105      doc = reference_filter("See #{reference}")
106      link = doc.css('a').first
107
108      expect(link).to have_attribute('data-merge-request')
109      expect(link.attr('data-merge-request')).to eq merge.id.to_s
110    end
111
112    it 'includes a data-reference-format attribute' do
113      doc = reference_filter("Merge #{reference}+")
114      link = doc.css('a').first
115
116      expect(link).to have_attribute('data-reference-format')
117      expect(link.attr('data-reference-format')).to eq('+')
118    end
119
120    it 'includes a data-reference-format attribute for URL references' do
121      doc = reference_filter("Merge #{urls.project_merge_request_url(project, merge)}+")
122      link = doc.css('a').first
123
124      expect(link).to have_attribute('data-reference-format')
125      expect(link.attr('data-reference-format')).to eq('+')
126    end
127
128    it 'supports an :only_path context' do
129      doc = reference_filter("Merge #{reference}", only_path: true)
130      link = doc.css('a').first.attr('href')
131
132      expect(link).not_to match %r(https?://)
133      expect(link).to eq urls.project_merge_request_url(project, merge, only_path: true)
134    end
135  end
136
137  context 'cross-project / cross-namespace complete reference' do
138    let(:project2)          { create(:project, :public) }
139    let(:merge)             { create(:merge_request, source_project: project2) }
140    let(:reference)         { "#{project2.full_path}!#{merge.iid}" }
141
142    it 'links to a valid reference' do
143      doc = reference_filter("See #{reference}")
144
145      expect(doc.css('a').first.attr('href'))
146        .to eq urls.project_merge_request_url(project2, merge)
147    end
148
149    it 'link has valid text' do
150      doc = reference_filter("Merge (#{reference}.)")
151
152      expect(doc.css('a').first.text).to eq(reference)
153    end
154
155    it 'has valid text' do
156      doc = reference_filter("Merge (#{reference}.)")
157
158      expect(doc.text).to eq("Merge (#{reference}.)")
159    end
160
161    it 'has correct data attributes' do
162      doc = reference_filter("Merge (#{reference}.)")
163
164      link = doc.css('a').first
165
166      expect(link.attr('data-project')).to eq project2.id.to_s
167      expect(link.attr('data-project-path')).to eq project2.full_path
168      expect(link.attr('data-iid')).to eq merge.iid.to_s
169      expect(link.attr('data-mr-title')).to eq merge.title
170    end
171
172    it 'ignores invalid merge IDs on the referenced project' do
173      exp = act = "Merge #{invalidate_reference(reference)}"
174
175      expect(reference_filter(act).to_html).to eq exp
176    end
177  end
178
179  context 'cross-project / same-namespace complete reference' do
180    let(:namespace) { create(:namespace) }
181    let(:project)   { create(:project, :public, namespace: namespace) }
182    let(:project2)  { create(:project, :public, namespace: namespace) }
183    let!(:merge)    { create(:merge_request, source_project: project2) }
184    let(:reference) { "#{project2.full_path}!#{merge.iid}" }
185
186    it 'links to a valid reference' do
187      doc = reference_filter("See #{reference}")
188
189      expect(doc.css('a').first.attr('href'))
190        .to eq urls.project_merge_request_url(project2, merge)
191    end
192
193    it 'link has valid text' do
194      doc = reference_filter("Merge (#{reference}.)")
195
196      expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}")
197    end
198
199    it 'has valid text' do
200      doc = reference_filter("Merge (#{reference}.)")
201
202      expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)")
203    end
204
205    it 'ignores invalid merge IDs on the referenced project' do
206      exp = act = "Merge #{invalidate_reference(reference)}"
207
208      expect(reference_filter(act).to_html).to eq exp
209    end
210  end
211
212  context 'cross-project shorthand reference' do
213    let(:namespace) { create(:namespace) }
214    let(:project)   { create(:project, :public, namespace: namespace) }
215    let(:project2)  { create(:project, :public, namespace: namespace) }
216    let!(:merge)    { create(:merge_request, source_project: project2) }
217    let(:reference) { "#{project2.path}!#{merge.iid}" }
218
219    it 'links to a valid reference' do
220      doc = reference_filter("See #{reference}")
221
222      expect(doc.css('a').first.attr('href'))
223        .to eq urls.project_merge_request_url(project2, merge)
224    end
225
226    it 'link has valid text' do
227      doc = reference_filter("Merge (#{reference}.)")
228
229      expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}")
230    end
231
232    it 'has valid text' do
233      doc = reference_filter("Merge (#{reference}.)")
234
235      expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)")
236    end
237
238    it 'ignores invalid merge IDs on the referenced project' do
239      exp = act = "Merge #{invalidate_reference(reference)}"
240
241      expect(reference_filter(act).to_html).to eq exp
242    end
243  end
244
245  context 'URL reference for a commit' do
246    let(:mr) { create(:merge_request) }
247    let(:reference) do
248      urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}"
249    end
250
251    let(:commit) { mr.commits.find { |commit| commit.sha == mr.diff_head_sha } }
252
253    it 'links to a valid reference' do
254      doc = reference_filter("See #{reference}")
255
256      expect(doc.css('a').first.attr('href'))
257        .to eq reference
258    end
259
260    it 'commit ref tag is valid' do
261      doc = reference_filter("See #{reference}")
262      commit_ref_tag = doc.css('a').first.css('span.gfm.gfm-commit')
263
264      expect(commit_ref_tag.text).to eq(commit.short_id)
265    end
266
267    it 'has valid text' do
268      doc = reference_filter("See #{reference}")
269
270      expect(doc.text).to eq("See #{mr.to_reference(full: true)} (#{commit.short_id})")
271    end
272
273    it 'has valid title attribute' do
274      doc = reference_filter("See #{reference}")
275
276      expect(doc.css('a').first.attr('title')).to eq(commit.title)
277    end
278
279    it 'ignores invalid commit short_ids on link text' do
280      invalidate_commit_reference =
281        urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=12345678"
282      doc = reference_filter("See #{invalidate_commit_reference}")
283
284      expect(doc.text).to eq("See #{mr.to_reference(full: true)} (diffs)")
285    end
286  end
287
288  context 'cross-project URL reference' do
289    let(:namespace) { create(:namespace, name: 'cross-reference') }
290    let(:project2)  { create(:project, :public, namespace: namespace) }
291    let(:merge)     { create(:merge_request, source_project: project2, target_project: project2) }
292    let(:reference) { urls.project_merge_request_url(project2, merge) + '/diffs#note_123' }
293
294    it 'links to a valid reference' do
295      doc = reference_filter("See #{reference}")
296
297      expect(doc.css('a').first.attr('href'))
298        .to eq reference
299    end
300
301    it 'links with adjacent text' do
302      doc = reference_filter("Merge (#{reference}.)")
303      expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)</a>\.\)})
304    end
305  end
306
307  context 'group context' do
308    it 'links to a valid reference' do
309      reference = "#{project.full_path}!#{merge.iid}"
310
311      result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
312
313      expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge))
314    end
315  end
316end
317