From bc5cfafb1cc5207a372face97c86ea6d9b57c5cd Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sat, 31 Oct 2026 19:23:52 +1300 Subject: [PATCH] Implement unsubscribe functionality and add tests for email unsubscription --- app/controllers/members_controller.rb | 7 ++-- spec/requests/unsubscribe_spec.rb | 51 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 spec/requests/unsubscribe_spec.rb diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 9373b6a3d..48d1b8336 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -58,10 +58,13 @@ class MembersController < ApplicationController def unsubscribe verifier = ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_SECRET_TOKEN', nil)) - decrypted_message = verifier.verify(params[:message]) + # The message is JSON, so its keys and the type come back as strings. + decrypted_message = verifier.verify(params[:message]).with_indifferent_access @member = Member.find(decrypted_message[:member_id]) - @type = decrypted_message[:type] + @type = decrypted_message[:type].to_sym + raise ActiveSupport::MessageVerifier::InvalidSignature unless EMAIL_TYPE_STRING.key?(@type) + @member.update(@type => false) flash.now[:notice] = I18n.t('members.unsubscribed', email_type: EMAIL_TYPE_STRING[@type]) diff --git a/spec/requests/unsubscribe_spec.rb b/spec/requests/unsubscribe_spec.rb new file mode 100644 index 000000000..cc1236a9e --- /dev/null +++ b/spec/requests/unsubscribe_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Unsubscribing from emails' do + let(:member) { create(:member) } + let(:verifier) { ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_SECRET_TOKEN', nil)) } + + def link_in(mail, text) + Nokogiri::HTML(mail.body.encoded).css('a').find { |anchor| anchor.text.include?(text) }['href'] + end + + def unsubscribe_via(mail, text) + get URI(link_in(mail, text)).request_uri + end + + context 'with the link in the planting reminder' do + before { unsubscribe_via(NotifierMailer.planting_reminder(member), 'Unsubscribe from planting reminders') } + + it { expect(response).to have_http_status(:ok) } + it { expect(response.body).to include 'unsubscribed from planting reminders' } + it { expect(member.reload.send_planting_reminder).to be false } + it { expect(member.reload.send_harvest_reminder).to be true } + it { expect(member.reload.send_notification_email).to be true } + end + + context 'with the link in the harvest reminder' do + let(:crop) { create(:crop, median_days_to_first_harvest: 20) } + + before do + create(:planting, owner: member, crop:, planted_at: 15.days.ago) + unsubscribe_via(NotifierMailer.harvest_reminder(member), 'Unsubscribe from harvest reminders') + end + + it { expect(member.reload.send_harvest_reminder).to be false } + it { expect(member.reload.send_planting_reminder).to be true } + end + + context 'with a signed message for a column that is not an email setting' do + before { get unsubscribe_member_path(verifier.generate({ member_id: member.id, type: :email })) } + + it { expect(response.body).to include 'there was an error' } + it { expect(member.reload.email).to eq member.email } + end + + context 'with a message that was not signed by us' do + before { get unsubscribe_member_path('not-signed') } + + it { expect(response.body).to include 'there was an error' } + end +end