diff --git a/app/controllers/unsubscribe_controller.rb b/app/controllers/unsubscribe_controller.rb new file mode 100644 index 0000000000..b0f7ae2efc --- /dev/null +++ b/app/controllers/unsubscribe_controller.rb @@ -0,0 +1,37 @@ +class UnsubscribeController < ApplicationController + skip_forgery_protection + + def show + @form = Unsubscribe::ConfirmationForm.new(form_params) + + if @form.reminder.nil? + render "subscription_not_found" + end + end + + def create + @form = Unsubscribe::ConfirmationForm.new(form_params) + + if @form.reminder.nil? + head :bad_request + else + @form.reminder.destroy + end + end + + private + + def form_params + params.permit([:id]) + end + + def current_journey_routing_name + params[:journey] + end + helper_method :current_journey_routing_name + + def current_journey + Journeys.for_routing_name(params[:journey]) + end + helper_method :current_journey +end diff --git a/app/forms/unsubscribe/confirmation_form.rb b/app/forms/unsubscribe/confirmation_form.rb new file mode 100644 index 0000000000..726180ac3c --- /dev/null +++ b/app/forms/unsubscribe/confirmation_form.rb @@ -0,0 +1,29 @@ +module Unsubscribe + class ConfirmationForm + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :id, :string + + def reminder + @reminder ||= Reminder.find_by(id:) + end + + def obfuscasted_email + head, tail = reminder.email_address.split("@") + + mask = case head.size + when 1, 2, 3 + "*" * head.size + else + [head[0], "*" * (head.length - 2), head[-1]].join + end + + [mask, "@", tail].join + end + + def journey_name + I18n.t("journey_name", scope: reminder.journey::I18N_NAMESPACE).downcase + end + end +end diff --git a/app/mailers/reminder_mailer.rb b/app/mailers/reminder_mailer.rb index e48a712a07..54313f5cc6 100644 --- a/app/mailers/reminder_mailer.rb +++ b/app/mailers/reminder_mailer.rb @@ -19,7 +19,13 @@ def email_verification(reminder, one_time_password, journey_name) journey_name: } - send_mail(OTP_EMAIL_NOTIFY_TEMPLATE_ID, personalisation) + template_mail( + OTP_EMAIL_NOTIFY_TEMPLATE_ID, + to: @reminder.email_address, + reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID, + subject: @subject, + personalisation: + ) end def reminder_set(reminder) @@ -29,10 +35,17 @@ def reminder_set(reminder) personalisation = { first_name: extract_first_name(@reminder.full_name), support_email_address: support_email_address, - next_application_window: @reminder.send_year + next_application_window: @reminder.send_year, + unsubscribe_url: unsubscribe_url(reminder:) } - send_mail(REMINDER_SET_NOTIFY_TEMPLATE_ID, personalisation) + template_mail( + REMINDER_SET_NOTIFY_TEMPLATE_ID, + to: @reminder.email_address, + reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID, + subject: @subject, + personalisation: + ) end # TODO: This template only accommodates LUP/ECP claims currently. Needs to @@ -47,22 +60,22 @@ def reminder(reminder) support_email_address: support_email_address } - send_mail(REMINDER_APPLICATION_WINDOW_OPEN_NOTIFY_TEMPLATE_ID, personalisation) - end - - private - - def extract_first_name(fullname) - (fullname || "").split(" ").first - end - - def send_mail(template_id, personalisation) template_mail( - template_id, + REMINDER_APPLICATION_WINDOW_OPEN_NOTIFY_TEMPLATE_ID, to: @reminder.email_address, reply_to_id: GENERIC_NOTIFY_REPLY_TO_ID, subject: @subject, personalisation: ) end + + private + + def unsubscribe_url(reminder:) + "https://#{ENV["CANONICAL_HOSTNAME"]}/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/#{reminder.id}" + end + + def extract_first_name(fullname) + (fullname || "").split(" ").first + end end diff --git a/app/views/unsubscribe/create.html.erb b/app/views/unsubscribe/create.html.erb new file mode 100644 index 0000000000..4a138d01e6 --- /dev/null +++ b/app/views/unsubscribe/create.html.erb @@ -0,0 +1,11 @@ +<%= content_for(:page_title) { "Unsubscribe" } %> + +
+
+ <%= govuk_panel(title_text: "Unsubscribe complete") %> + +

+ You will no longer receive your <%= @form.journey_name %> reminder to <%= @form.obfuscasted_email %>. +

+
+
diff --git a/app/views/unsubscribe/show.html.erb b/app/views/unsubscribe/show.html.erb new file mode 100644 index 0000000000..4011922864 --- /dev/null +++ b/app/views/unsubscribe/show.html.erb @@ -0,0 +1,27 @@ +<%= content_for(:page_title) { "Unsubscribe" } %> + +
+
+ <%= form_with model: @form, + url: unsubscribe_index_path, + scope: "", + builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %> + + <%= f.hidden_field :id %> + +

+ Are you sure you wish to unsubscribe from your <%= @form.journey_name %> reminder? +

+ +

+ You will no longer receive your reminder to <%= @form.obfuscasted_email %>. +

+ +

+ However, you will still continue to receive updates about your claim as it progresses. +

+ + <%= f.govuk_submit "Unsubscribe" %> + <% end %> +

+
diff --git a/app/views/unsubscribe/subscription_not_found.html.erb b/app/views/unsubscribe/subscription_not_found.html.erb new file mode 100644 index 0000000000..dc26268a90 --- /dev/null +++ b/app/views/unsubscribe/subscription_not_found.html.erb @@ -0,0 +1,15 @@ +
+
+

+ We can’t find your subscription +

+ +

+ You may have already unsubscribed. If you haven’t, check your email and copy the link again. +

+ +

+ If you need more help, contact support at <%= govuk_mail_to I18n.t("support_email_address", scope: [current_journey::I18N_NAMESPACE]), I18n.t("support_email_address", scope: [current_journey::I18N_NAMESPACE]) %> +

+
+
diff --git a/config/environments/development.rb b/config/environments/development.rb index d7b3cb6f23..c656970451 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -60,8 +60,7 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true - # Raise an exception if parameters that are not explicitly permitted are found - config.action_controller.action_on_unpermitted_parameters = :raise + config.action_controller.action_on_unpermitted_parameters = :log # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large diff --git a/config/environments/test.rb b/config/environments/test.rb index a92b501287..1601e36b07 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -33,8 +33,7 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Raise an exception if parameters that are not explicitly permitted are found - config.action_controller.action_on_unpermitted_parameters = :raise + config.action_controller.action_on_unpermitted_parameters = :log config.action_mailer.perform_caching = false diff --git a/config/routes.rb b/config/routes.rb index 3d62b88694..732803e7f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,11 @@ def matches?(request) end end + resources :unsubscribe, + path: "/unsubscribe/:resource_class", + constraints: {resource_class: /reminders/}, + only: [:create, :show] + scope constraints: {journey: /further-education-payments|additional-payments/} do resources :reminders, only: [:show, :update], diff --git a/spec/features/unsubscribe_spec.rb b/spec/features/unsubscribe_spec.rb new file mode 100644 index 0000000000..7b7e742af6 --- /dev/null +++ b/spec/features/unsubscribe_spec.rb @@ -0,0 +1,26 @@ +require "rails_helper" + +RSpec.feature "unsubscribe from reminders" do + let(:reminder) do + create( + :reminder, + journey_class: Journeys::FurtherEducationPayments.to_s + ) + end + + scenario "happy path" do + visit "/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/#{reminder.id}" + expect(page).to have_content "Are you sure you wish to unsub" + + expect { + click_button("Unsubscribe") + }.to change(Reminder, :count).by(-1) + + expect(page).to have_content "Unsubscribe complete" + end + + scenario "when reminder does not exist" do + visit "/#{reminder.journey::ROUTING_NAME}/unsubscribe/reminders/idonotexist" + expect(page).to have_content "We can’t find your subscription" + end +end diff --git a/spec/forms/current_school_form_spec.rb b/spec/forms/current_school_form_spec.rb index ac9afa847e..b68836251f 100644 --- a/spec/forms/current_school_form_spec.rb +++ b/spec/forms/current_school_form_spec.rb @@ -19,14 +19,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#schools" do context "new form" do let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) } diff --git a/spec/forms/employed_directly_form_spec.rb b/spec/forms/employed_directly_form_spec.rb index 6ecda4cf73..55cf332164 100644 --- a/spec/forms/employed_directly_form_spec.rb +++ b/spec/forms/employed_directly_form_spec.rb @@ -17,14 +17,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#employed_directly" do let(:params) { ActionController::Parameters.new({slug:, claim: {}}) } diff --git a/spec/forms/form_spec.rb b/spec/forms/form_spec.rb index ebfa528cd0..6df0bb5002 100644 --- a/spec/forms/form_spec.rb +++ b/spec/forms/form_spec.rb @@ -48,14 +48,6 @@ def initialize(journey_session) let(:claim_params) { {first_name: "test-name"} } describe "#initialize" do - context "with unpermitted params" do - let(:claim_params) { {unpermitted: "my-name"} } - - it "raises an error" do - expect { form }.to raise_error(ActionController::UnpermittedParameters) - end - end - context "with valid params" do let(:claim_params) { {first_name: "my-name"} } @@ -218,13 +210,5 @@ def initialize(journey_session) expect(form.permitted_params).to eql(ActionController::Parameters.new(claim_params.stringify_keys).permit!) end end - - context "with params containing attributes not defined on the form" do - let(:claim_params) { super().merge(unpermitted_attribute: "test-value") } - - it "raises an error" do - expect { form.permitted_params }.to raise_error(ActionController::UnpermittedParameters) - end - end end end diff --git a/spec/forms/gender_form_spec.rb b/spec/forms/gender_form_spec.rb index 44b1587993..f31b88f025 100644 --- a/spec/forms/gender_form_spec.rb +++ b/spec/forms/gender_form_spec.rb @@ -25,14 +25,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#payroll_gender" do let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) } diff --git a/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb index 6fd7e257a0..3c29ae7d14 100644 --- a/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb +++ b/spec/forms/journeys/additional_payments_for_teaching/entire_term_contract_form_spec.rb @@ -17,14 +17,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#has_entire_term_contract" do let(:params) { ActionController::Parameters.new({slug:, claim: {}}) } diff --git a/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb index 01f2e4bf20..285f522d93 100644 --- a/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb +++ b/spec/forms/journeys/additional_payments_for_teaching/induction_completed_form_spec.rb @@ -17,14 +17,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#save" do let(:params) { ActionController::Parameters.new({slug: slug, claim: {induction_completed: true}}) } diff --git a/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb b/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb index 7cbdd1c6d6..676285cbbe 100644 --- a/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb +++ b/spec/forms/journeys/additional_payments_for_teaching/itt_academic_year_form_spec.rb @@ -31,14 +31,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#itt_academic_year" do let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) } diff --git a/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb b/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb index 8a99a0d7b5..ed75fd423a 100644 --- a/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb +++ b/spec/forms/journeys/teacher_student_loan_reimbursement/claim_school_form_spec.rb @@ -34,14 +34,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#schools" do context "new form" do let(:params) { ActionController::Parameters.new({slug: slug, claim: {}}) } diff --git a/spec/forms/personal_details_form_spec.rb b/spec/forms/personal_details_form_spec.rb index ad4a4c59cb..4aded18886 100644 --- a/spec/forms/personal_details_form_spec.rb +++ b/spec/forms/personal_details_form_spec.rb @@ -30,16 +30,6 @@ ) end - context "unpermitted claim param" do - let(:params) { {nonsense_id: 1} } - let(:logged_in_with_tid) { nil } - let(:teacher_id_user_info) { {} } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#show_name_section?" do context "when not logged_in_with_tid" do let(:logged_in_with_tid) { nil } diff --git a/spec/forms/poor_performance_form_spec.rb b/spec/forms/poor_performance_form_spec.rb index d4cf89040b..5f7d55b42d 100644 --- a/spec/forms/poor_performance_form_spec.rb +++ b/spec/forms/poor_performance_form_spec.rb @@ -11,14 +11,6 @@ it { expect(form).to be_a(Form) } - context "with unpermitted params" do - let(:claim_params) { {unpermitted_param: ""} } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "validations" do context "subject_to_formal_performance_action" do it "cannot be nil" do diff --git a/spec/forms/provide_mobile_number_form_spec.rb b/spec/forms/provide_mobile_number_form_spec.rb index 4b160b1cfd..02ceb4f9ca 100644 --- a/spec/forms/provide_mobile_number_form_spec.rb +++ b/spec/forms/provide_mobile_number_form_spec.rb @@ -29,16 +29,6 @@ ) end - context "unpermitted claim param" do - let(:provide_mobile_number) { nil } - - let(:params) { ActionController::Parameters.new({slug: slug, claim: {nonsense_id: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "validations" do let(:provide_mobile_number) { nil } diff --git a/spec/forms/supply_teacher_form_spec.rb b/spec/forms/supply_teacher_form_spec.rb index 52d2f33ace..6969780ec7 100644 --- a/spec/forms/supply_teacher_form_spec.rb +++ b/spec/forms/supply_teacher_form_spec.rb @@ -17,14 +17,6 @@ ) end - context "unpermitted claim param" do - let(:params) { ActionController::Parameters.new({slug:, claim: {random_param: 1}}) } - - it "raises an error" do - expect { form }.to raise_error ActionController::UnpermittedParameters - end - end - describe "#employed_as_supply_teacher" do let(:params) { ActionController::Parameters.new({slug:, claim: {}}) } diff --git a/spec/forms/unsubscribe/confirmation_form_spec.rb b/spec/forms/unsubscribe/confirmation_form_spec.rb new file mode 100644 index 0000000000..9ae101be09 --- /dev/null +++ b/spec/forms/unsubscribe/confirmation_form_spec.rb @@ -0,0 +1,46 @@ +require "rails_helper" + +RSpec.describe Unsubscribe::ConfirmationForm do + describe "#obfuscasted_email" do + let(:reminder) { + create( + :reminder, + email_address: + ) + } + + subject { described_class.new(id: reminder.id) } + + context "happy path" do + let(:email_address) { "hello@example.com" } + + it "obfuscates" do + expect(subject.obfuscasted_email).to eql "h***o@example.com" + end + end + + context "1 char email" do + let(:email_address) { "h@example.com" } + + it "obfuscates" do + expect(subject.obfuscasted_email).to eql "*@example.com" + end + end + + context "2 char email" do + let(:email_address) { "hh@example.com" } + + it "obfuscates" do + expect(subject.obfuscasted_email).to eql "**@example.com" + end + end + + context "3 char email" do + let(:email_address) { "hah@example.com" } + + it "obfuscates" do + expect(subject.obfuscasted_email).to eql "***@example.com" + end + end + end +end diff --git a/spec/mailers/reminder_mailer_spec.rb b/spec/mailers/reminder_mailer_spec.rb new file mode 100644 index 0000000000..ad94f31a3f --- /dev/null +++ b/spec/mailers/reminder_mailer_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +RSpec.describe ReminderMailer do + let(:reminder) { + create(:reminder, :with_fe_reminder) + } + + describe "#reminder_set" do + it "includes unsubscribe_url in personalisation" do + mail = described_class.reminder_set(reminder) + + expect(mail.personalisation[:unsubscribe_url]).to eql("https://www.example.com/further-education-payments/unsubscribe/reminders/#{reminder.id}") + end + end +end diff --git a/spec/requests/admin/admin_amendments_spec.rb b/spec/requests/admin/admin_amendments_spec.rb index ac416edf14..20c9868604 100644 --- a/spec/requests/admin/admin_amendments_spec.rb +++ b/spec/requests/admin/admin_amendments_spec.rb @@ -87,15 +87,11 @@ expect(response.body).to include("To amend the claim you must change at least one value") end - it "raises an error and does not create an amendment or update the claim when attempting to modify an attribute that isn't in the allowed list" do + it "does not create an amendment or update the claim when attempting to modify an attribute that isn't in the allowed list" do original_counts = [claim.reference, claim.amendments.size] - expect { - post admin_claim_amendments_url(claim, amendment: {claim: {reference: "REF12345"}, - notes: "Claimant made a typo"}) - }.to raise_error( - ActionController::UnpermittedParameters - ) + post admin_claim_amendments_url(claim, amendment: {claim: {reference: "REF12345"}, + notes: "Claimant made a typo"}) expect([claim.reference, claim.amendments.size]).to eq(original_counts) end diff --git a/spec/requests/unsubscribe_spec.rb b/spec/requests/unsubscribe_spec.rb new file mode 100644 index 0000000000..053e929d26 --- /dev/null +++ b/spec/requests/unsubscribe_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe "unsubscribe", type: :request do + let!(:reminder) { create(:reminder, :with_fe_reminder) } + + describe "POST #create" do + context "happy path" do + it "unsubscribes from reminder via one click unsubscribe" do + expect { + post "/further-education-payments/unsubscribe/reminders", params: {id: reminder.id} + }.to change(Reminder, :count).by(-1) + + expect(response).to be_successful + end + end + + context "when no such reminder" do + it "returns 400 error" do + post "/further-education-payments/unsubscribe/reminders", params: {id: "idonotexist"} + + expect(response).to be_bad_request + end + end + end +end