diff --git a/.rubocop.yml b/.rubocop.yml index 52c97e23501..00d3129d03b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,7 @@ require: - rubocop-rails - rubocop-rspec - rubocop-performance + - ./lib/linters/i18n_helper_html_linter.rb - ./lib/linters/analytics_event_name_linter.rb - ./lib/linters/localized_validation_message_linter.rb - ./lib/linters/image_size_linter.rb @@ -45,6 +46,13 @@ Bundler/InsecureProtocolSource: Gemspec/DuplicatedAssignment: Enabled: true +IdentityIdp/I18nHelperHtmlLinter: + Enabled: true + Include: + - app/views/**/*.erb + - app/components/**/*.erb + - app/controllers/**/*.rb + IdentityIdp/AnalyticsEventNameLinter: Enabled: true Include: diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index 00a5134e774..ed505209855 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -4,7 +4,7 @@ <% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>
- <%= I18n.t('help_text.select_preferred_email_html', sp: @sp_name) %> + <%= t('help_text.select_preferred_email_html', sp: @sp_name) %>
<%= simple_form_for(@select_email_form, url: sign_up_select_email_path) do |f| %> diff --git a/lib/linters/i18n_helper_html_linter.rb b/lib/linters/i18n_helper_html_linter.rb new file mode 100644 index 00000000000..53251a13b05 --- /dev/null +++ b/lib/linters/i18n_helper_html_linter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module IdentityIdp + # This linter checks to ensure that strings which include HTML are rendered using Rails `t` + # view helper, rather than through the I18n class. Only the Rails view helper will mark the + # content as HTML-safe. + # + # @see https://guides.rubyonrails.org/i18n.html#using-safe-html-translations + # + # @example + # # bad + # I18n.t('errors.message_html') + # + # # good + # t('errors.message_html') + # + class I18nHelperHtmlLinter < RuboCop::Cop::Base + MSG = 'Use the Rails `t` view helper for HTML-safe strings' + + RESTRICT_ON_SEND = [:t].freeze + + def_node_matcher :i18n_class_send?, <<~PATTERN + (send (const nil? :I18n) :t $...) + PATTERN + + def on_send(node) + return if !i18n_class_send?(node) || !i18n_key(node)&.end_with?('_html') + add_offense(node) + end + + private + + def i18n_key(node) + first_argument = node.arguments.first + return if first_argument.nil? + return if !first_argument.respond_to?(:value) + first_argument.value.to_s + end + end + end + end +end diff --git a/spec/lib/linters/i18n_helper_html_linter_spec.rb b/spec/lib/linters/i18n_helper_html_linter_spec.rb new file mode 100644 index 00000000000..7cd2f0ec4a0 --- /dev/null +++ b/spec/lib/linters/i18n_helper_html_linter_spec.rb @@ -0,0 +1,51 @@ +require 'rubocop' +require 'rubocop/rspec/cop_helper' +require 'rubocop/rspec/expect_offense' + +require_relative '../../../lib/linters/i18n_helper_html_linter' + +RSpec.describe RuboCop::Cop::IdentityIdp::I18nHelperHtmlLinter do + include CopHelper + include RuboCop::RSpec::ExpectOffense + + let(:config) { RuboCop::Config.new } + let(:cop) { RuboCop::Cop::IdentityIdp::I18nHelperHtmlLinter.new(config) } + + it 'registers offense when calling `t` from i18n class with key suffixed by "_html"' do + expect_offense(<<~RUBY) + I18n.t('errors.message_html') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/I18nHelperHtmlLinter: Use the Rails `t` view helper for HTML-safe strings + RUBY + end + + it 'registers offense when calling `t` from i18n class with symbol key suffixed by "_html"' do + expect_offense(<<~RUBY) + I18n.t(:message_html) + ^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/I18nHelperHtmlLinter: Use the Rails `t` view helper for HTML-safe strings + RUBY + end + + it 'gracefully handles `I18n.t` without arguments' do + expect_no_offenses(<<~RUBY) + I18n.t + RUBY + end + + it 'gracefully handles `I18n.t` with variable key' do + expect_no_offenses(<<~RUBY) + I18n.t(key) + RUBY + end + + it 'registers no offense when calling `t` from i18n class with key not suffixed by "_html"' do + expect_no_offenses(<<~RUBY) + I18n.t('errors.message') + RUBY + end + + it 'registers no offense when calling `t` from Rails view helper' do + expect_no_offenses(<<~RUBY) + t('errors.message_html') + RUBY + end +end diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb index 4645965a8ed..454ddb9144b 100644 --- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb @@ -22,6 +22,12 @@ @select_email_form = SelectEmailForm.new(user:, identity:) end + it 'renders introduction text' do + expect(rendered).to have_content( + strip_tags(t('help_text.select_preferred_email_html', sp: identity.display_name)), + ) + end + it 'renders a list of the users email addresses as radio options' do allow(self).to receive(:page).and_return(Capybara.string(rendered)) inputs = page.find_all('[type="radio"]') diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb index 9f360eddf30..30d16c34b92 100644 --- a/spec/views/sign_up/select_email/show.html.erb_spec.rb +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe 'sign_up/select_email/show.html.erb' do + subject(:rendered) { render } let(:email) { 'michael.motorist@email.com' } let(:email2) { 'michael.motorist2@email.com' } let(:user) { create(:user) } @@ -10,11 +11,16 @@ create(:email_address, email: email2, user:) @user_emails = user.confirmed_email_addresses @select_email_form = SelectEmailForm.new(user:) + @sp_name = 'Test Service Provider' end - it 'shows all of the user\'s emails' do - render + it 'renders introduction text' do + expect(rendered).to have_content( + strip_tags(t('help_text.select_preferred_email_html', sp: @sp_name)), + ) + end + it 'shows all of the emails' do expect(rendered).to include('michael.motorist@email.com') expect(rendered).to include('michael.motorist2@email.com') end