1# frozen_string_literal: true
2
3require 'spec_helper'
4
5RSpec.describe EmailsHelper do
6  describe 'closure_reason_text' do
7    context 'when given a MergeRequest' do
8      let(:merge_request) { create(:merge_request) }
9      let(:merge_request_presenter) { merge_request.present }
10
11      context 'when user can read merge request' do
12        let(:user) { create(:user) }
13
14        before do
15          merge_request.project.add_developer(user)
16          self.instance_variable_set(:@recipient, user)
17          self.instance_variable_set(:@project, merge_request.project)
18        end
19
20        context "and format is text" do
21          it "returns plain text" do
22            expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
23          end
24        end
25
26        context "and format is HTML" do
27          it "returns HTML" do
28            expect(helper.closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
29          end
30        end
31
32        context "and format is unknown" do
33          it "returns plain text" do
34            expect(helper.closure_reason_text(merge_request, format: 'unknown')).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
35          end
36        end
37      end
38
39      context 'when user cannot read merge request' do
40        it "does not have link to merge request" do
41          expect(helper.closure_reason_text(merge_request)).to be_empty
42        end
43      end
44    end
45
46    context 'when given a String' do
47      let(:user) { create(:user) }
48      let(:project) { create(:project) }
49      let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
50
51      context 'when user can read commits' do
52        before do
53          project.add_developer(user)
54          self.instance_variable_set(:@recipient, user)
55          self.instance_variable_set(:@project, project)
56        end
57
58        it "returns plain text" do
59          expect(closure_reason_text(closed_via)).to eq("via #{closed_via}")
60        end
61      end
62
63      context 'when user cannot read commits' do
64        it "returns plain text" do
65          expect(closure_reason_text(closed_via)).to be_empty
66        end
67      end
68    end
69
70    context 'when not given anything' do
71      it "returns empty string" do
72        expect(closure_reason_text(nil)).to eq("")
73      end
74    end
75  end
76
77  describe 'notification_reason_text' do
78    subject { helper.notification_reason_text(reason_code) }
79
80    using RSpec::Parameterized::TableSyntax
81
82    where(:reason_code, :reason_text) do
83      NotificationReason::OWN_ACTIVITY | ' of your activity '
84      NotificationReason::ASSIGNED     | ' you have been assigned an item '
85      NotificationReason::MENTIONED    | ' you have been mentioned '
86      ""                               | ' of your account '
87      nil                              | ' of your account '
88    end
89
90    with_them do
91      it { is_expected.to start_with "You're receiving this email because" }
92
93      it { is_expected.to include reason_text }
94
95      it { is_expected.to end_with "on #{Gitlab.config.gitlab.host}." }
96    end
97  end
98
99  describe 'sanitize_name' do
100    context 'when name contains a valid URL string' do
101      it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do
102        expect(sanitize_name('https://about.gitlab.com')).to eq('https://about_gitlab_com')
103        expect(sanitize_name('www.gitlab.com')).to eq('www_gitlab_com')
104        expect(sanitize_name('//about.gitlab.com/handbook/security/#best-practices')).to eq('//about_gitlab_com/handbook/security/#best-practices')
105      end
106
107      it 'returns name as it is when it does not contain a URL' do
108        expect(sanitize_name('Foo Bar')).to eq('Foo Bar')
109      end
110    end
111  end
112
113  describe '#say_hi' do
114    let(:user) { create(:user, name: 'John') }
115
116    it 'returns the greeting message for the given user' do
117      expect(say_hi(user)).to eq('Hi John!')
118    end
119  end
120
121  describe '#say_hello' do
122    let(:user) { build(:user, name: 'John') }
123
124    it 'returns the greeting message for the given user' do
125      expect(say_hello(user)).to eq('Hello, John!')
126    end
127  end
128
129  describe '#two_factor_authentication_disabled_text' do
130    it 'returns the message that 2FA is disabled' do
131      expect(two_factor_authentication_disabled_text).to eq(
132        _('Two-factor authentication has been disabled for your GitLab account.')
133      )
134    end
135  end
136
137  describe '#re_enable_two_factor_authentication_text' do
138    context 'format is html' do
139      it 'returns HTML' do
140        expect(re_enable_two_factor_authentication_text(format: :html)).to eq(
141          "If you want to re-enable two-factor authentication, visit the " \
142          "#{link_to('two-factor authentication settings', profile_two_factor_auth_url, target: :_blank, rel: 'noopener noreferrer')} page."
143        )
144      end
145    end
146
147    context 'format is not specified' do
148      it 'returns text' do
149        expect(re_enable_two_factor_authentication_text).to eq(
150          "If you want to re-enable two-factor authentication, visit #{profile_two_factor_auth_url}"
151        )
152      end
153    end
154  end
155
156  describe '#admin_changed_password_text' do
157    context 'format is html' do
158      it 'returns HTML' do
159        expect(admin_changed_password_text(format: :html)).to eq(
160          "An administrator changed the password for your GitLab account on " \
161          "#{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url, target: :_blank, rel: 'noopener noreferrer')}."
162        )
163      end
164    end
165
166    context 'format is not specified' do
167      it 'returns text' do
168        expect(admin_changed_password_text).to eq(
169          "An administrator changed the password for your GitLab account on #{Gitlab.config.gitlab.url}."
170        )
171      end
172    end
173  end
174
175  describe '#contact_your_administrator_text' do
176    it 'returns the message to contact the administrator' do
177      expect(contact_your_administrator_text).to eq(
178        _('Please contact your administrator with any questions.')
179      )
180    end
181  end
182
183  describe 'password_reset_token_valid_time' do
184    def validate_time_string(time_limit, expected_string)
185      Devise.reset_password_within = time_limit
186      expect(password_reset_token_valid_time).to eq(expected_string)
187    end
188
189    context 'when time limit is less than 2 hours' do
190      it 'displays the time in hours using a singular unit' do
191        validate_time_string(1.hour, '1 hour')
192      end
193    end
194
195    context 'when time limit is 2 or more hours' do
196      it 'displays the time in hours using a plural unit' do
197        validate_time_string(2.hours, '2 hours')
198      end
199    end
200
201    context 'when time limit contains fractions of an hour' do
202      it 'rounds down to the nearest hour' do
203        validate_time_string(96.minutes, '1 hour')
204      end
205    end
206
207    context 'when time limit is 24 or more hours' do
208      it 'displays the time in days using a singular unit' do
209        validate_time_string(24.hours, '1 day')
210      end
211    end
212
213    context 'when time limit is 2 or more days' do
214      it 'displays the time in days using a plural unit' do
215        validate_time_string(2.days, '2 days')
216      end
217    end
218
219    context 'when time limit contains fractions of a day' do
220      it 'rounds down to the nearest day' do
221        validate_time_string(57.hours, '2 days')
222      end
223    end
224  end
225
226  describe '#header_logo' do
227    context 'there is a brand item with a logo' do
228      it 'returns the brand header logo' do
229        appearance = create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png')
230
231        expect(header_logo).to eq(
232          %{<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" />}
233        )
234      end
235    end
236
237    context 'there is a brand item without a logo' do
238      it 'returns the default header logo' do
239        create :appearance, header_logo: nil
240
241        expect(header_logo).to match(
242          %r{<img alt="GitLab" src="/images/mailers/gitlab_header_logo\.(?:gif|png)" width="\d+" height="\d+" />}
243        )
244      end
245    end
246
247    context 'there is no brand item' do
248      it 'returns the default header logo' do
249        expect(header_logo).to match(
250          %r{<img alt="GitLab" src="/images/mailers/gitlab_header_logo\.(?:gif|png)" width="\d+" height="\d+" />}
251        )
252      end
253    end
254  end
255
256  describe '#create_list_id_string' do
257    using RSpec::Parameterized::TableSyntax
258
259    where(:full_path, :list_id_path) do
260      "01234"  | "01234"
261      "5/0123" | "012.."
262      "45/012" | "012.."
263      "012"    | "012"
264      "23/01"  | "01.23"
265      "2/01"   | "01.2"
266      "234/01" | "01.."
267      "4/2/0"  | "0.2.4"
268      "45/2/0" | "0.2.."
269      "5/23/0" | "0.."
270      "0-2/5"  | "5.0-2"
271      "0_2/5"  | "5.0-2"
272      "0.2/5"  | "5.0-2"
273    end
274
275    with_them do
276      it 'ellipcizes different variants' do
277        project = double("project")
278        allow(project).to receive(:full_path).and_return(full_path)
279        allow(project).to receive(:id).and_return(12345)
280        # Set a max length that gives only 5 chars for the project full path
281        max_length = "12345..#{Gitlab.config.gitlab.host}".length + 5
282        list_id = create_list_id_string(project, max_length)
283
284        expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
285        expect(list_id).to satisfy { |s| s.length <= max_length }
286      end
287    end
288  end
289
290  describe 'Create realistic List-Id identifier' do
291    using RSpec::Parameterized::TableSyntax
292
293    where(:full_path, :list_id_path) do
294      "gitlab-org/gitlab-ce" | "gitlab-ce.gitlab-org"
295      "project-name/subproject_name/my.project" | "my-project.subproject-name.project-name"
296    end
297
298    with_them do
299      it 'produces the right List-Id' do
300        project = double("project")
301        allow(project).to receive(:full_path).and_return(full_path)
302        allow(project).to receive(:id).and_return(12345)
303        list_id = create_list_id_string(project)
304
305        expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
306        expect(list_id).to satisfy { |s| s.length <= 255 }
307      end
308    end
309  end
310
311  describe 'header and footer messages' do
312    context 'when email_header_and_footer_enabled is enabled' do
313      it 'returns header and footer messages' do
314        create :appearance, header_message: 'Foo', footer_message: 'Bar', email_header_and_footer_enabled: true
315
316        aggregate_failures do
317          expect(html_header_message).to eq(%{<div class="header-message" style=""><p>Foo</p></div>})
318          expect(html_footer_message).to eq(%{<div class="footer-message" style=""><p>Bar</p></div>})
319          expect(text_header_message).to eq('Foo')
320          expect(text_footer_message).to eq('Bar')
321        end
322      end
323
324      context 'when header and footer messages are empty' do
325        it 'returns nil' do
326          create :appearance, header_message: '', footer_message: '', email_header_and_footer_enabled: true
327
328          aggregate_failures do
329            expect(html_header_message).to eq(nil)
330            expect(html_footer_message).to eq(nil)
331            expect(text_header_message).to eq(nil)
332            expect(text_footer_message).to eq(nil)
333          end
334        end
335      end
336
337      context 'when header and footer messages are nil' do
338        it 'returns nil' do
339          create :appearance, header_message: nil, footer_message: nil, email_header_and_footer_enabled: true
340
341          aggregate_failures do
342            expect(html_header_message).to eq(nil)
343            expect(html_footer_message).to eq(nil)
344            expect(text_header_message).to eq(nil)
345            expect(text_footer_message).to eq(nil)
346          end
347        end
348      end
349    end
350
351    context 'when email_header_and_footer_enabled is disabled' do
352      it 'returns header and footer messages' do
353        create :appearance, header_message: 'Foo', footer_message: 'Bar', email_header_and_footer_enabled: false
354
355        aggregate_failures do
356          expect(html_header_message).to eq(nil)
357          expect(html_footer_message).to eq(nil)
358          expect(text_header_message).to eq(nil)
359          expect(text_footer_message).to eq(nil)
360        end
361      end
362    end
363  end
364
365  describe '#change_reviewer_notification_text' do
366    let(:mary) { build(:user, name: 'Mary') }
367    let(:john) { build(:user, name: 'John') }
368    let(:ted) { build(:user, name: 'Ted') }
369
370    context 'to new reviewers only' do
371      let(:previous_reviewers) { [] }
372      let(:new_reviewers) { [john] }
373
374      context 'with no html tag' do
375        let(:expected_output) do
376          'Reviewer changed to John'
377        end
378
379        it 'returns the expected output' do
380          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
381        end
382      end
383
384      context 'with <strong> tag' do
385        let(:expected_output) do
386          'Reviewer changed to <strong>John</strong>'
387        end
388
389        it 'returns the expected output' do
390          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
391        end
392      end
393    end
394
395    context 'from previous reviewers to new reviewers' do
396      let(:previous_reviewers) { [john, mary] }
397      let(:new_reviewers) { [ted] }
398
399      context 'with no html tag' do
400        let(:expected_output) do
401          'Reviewer changed from John and Mary to Ted'
402        end
403
404        it 'returns the expected output' do
405          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
406        end
407      end
408
409      context 'with <strong> tag' do
410        let(:expected_output) do
411          'Reviewer changed from <strong>John and Mary</strong> to <strong>Ted</strong>'
412        end
413
414        it 'returns the expected output' do
415          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
416        end
417      end
418    end
419
420    context 'from previous reviewers to no reviewers' do
421      let(:previous_reviewers) { [john, mary] }
422      let(:new_reviewers) { [] }
423
424      context 'with no html tag' do
425        let(:expected_output) do
426          'Reviewer changed from John and Mary to Unassigned'
427        end
428
429        it 'returns the expected output' do
430          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
431        end
432      end
433
434      context 'with <strong> tag' do
435        let(:expected_output) do
436          'Reviewer changed from <strong>John and Mary</strong> to <strong>Unassigned</strong>'
437        end
438
439        it 'returns the expected output' do
440          expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
441        end
442      end
443    end
444
445    context "with a <script> tag in user's name" do
446      let(:previous_reviewers) { [] }
447      let(:new_reviewers) { [fishy_user] }
448      let(:fishy_user) { build(:user, name: "<script>alert('hi')</script>") }
449
450      let(:expected_output) do
451        'Reviewer changed to <strong>&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</strong>'
452      end
453
454      it 'escapes the html tag' do
455        expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
456      end
457    end
458
459    context "with url in user's name" do
460      subject(:email_helper) { Object.new.extend(described_class) }
461
462      let(:previous_reviewers) { [] }
463      let(:new_reviewers) { [fishy_user] }
464      let(:fishy_user) { build(:user, name: "example.com") }
465
466      let(:expected_output) do
467        'Reviewer changed to example_com'
468      end
469
470      it "sanitizes user's name" do
471        expect(email_helper).to receive(:sanitize_name).and_call_original
472        expect(email_helper.change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
473      end
474    end
475  end
476end
477