Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/forms/register_user_email_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def process_successful_submission(request_id, instructions)
# already taken and if so, we act as if the user registration was successful.
if email_address_record&.user&.suspended?
send_suspended_user_email(email_address_record)
elsif blocked_email_address
send_suspended_user_email(blocked_email_address)
elsif email_taken? && user_unconfirmed?
update_user_language_preference
send_sign_up_unconfirmed_email(request_id)
Expand Down Expand Up @@ -175,4 +177,9 @@ def existing_user
def email_request_id(request_id)
request_id if request_id.present? && ServiceProviderRequestProxy.find_by(uuid: request_id)
end

def blocked_email_address
return @blocked_email_address if defined?(@blocked_email_address)
@blocked_email_address = SuspendedEmail.find_with_email(email)
end
end
3 changes: 3 additions & 0 deletions app/models/email_address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class EmailAddress < ApplicationRecord
belongs_to :user, inverse_of: :email_addresses
validates :encrypted_email, presence: true
validates :email_fingerprint, presence: true
# rubocop:disable Rails/HasManyOrHasOneDependent
has_one :suspended_email
# rubocop:enable Rails/HasManyOrHasOneDependent

scope :confirmed, -> { where('confirmed_at IS NOT NULL') }

Expand Down
22 changes: 22 additions & 0 deletions app/models/suspended_email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class SuspendedEmail < ApplicationRecord
belongs_to :email_address
validates :digested_base_email, presence: true

class << self
def generate_email_digest(email)
normalized_email = EmailNormalizer.new(email).normalized_email
OpenSSL::Digest::SHA256.hexdigest(normalized_email)
end

def create_from_email_adddress!(email_address)
create!(
digested_base_email: generate_email_digest(email_address.email),
email_address: email_address,
)
end

def find_with_email(email)
find_by(digested_base_email: generate_email_digest(email))&.email_address
end
end
end
6 changes: 6 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def suspend!
OutOfBandSessionAccessor.new(unique_session_id).destroy if unique_session_id
update!(suspended_at: Time.zone.now, unique_session_id: nil)
analytics.user_suspended(success: true)
email_addresses.map do |email_address|
SuspendedEmail.create_from_email_adddress!(email_address)
end
end

def reinstate!
Expand All @@ -131,6 +134,9 @@ def reinstate!
end
update!(reinstated_at: Time.zone.now)
analytics.user_reinstated(success: true)
email_addresses.map do |email_address|
SuspendedEmail.find_with_email(email_address.email)&.destroy
end
end

def pending_profile
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class CreateSuspendedEmailsTable < ActiveRecord::Migration[7.0]
def change
create_table :suspended_emails do |t|
t.references :email_address, null: false
t.string :digested_base_email, null: false, index: true
t.timestamps
end
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_07_07_144310) do
ActiveRecord::Schema[7.0].define(version: 2023_07_20_183509) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
Expand Down Expand Up @@ -572,6 +572,15 @@
t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true
end

create_table "suspended_emails", force: :cascade do |t|
t.bigint "email_address_id", null: false
t.string "digested_base_email", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["digested_base_email"], name: "index_suspended_emails_on_digested_base_email"
t.index ["email_address_id"], name: "index_suspended_emails_on_email_address_id"
end

create_table "users", id: :serial, force: :cascade do |t|
t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at", precision: nil
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/suspended_emails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :suspended_email do
digested_base_email { 'test_digest' }
association :email_address
end
end
28 changes: 27 additions & 1 deletion spec/forms/register_user_email_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
it_behaves_like 'email validation'

describe '#submit' do
let(:email_domain) { 'test.com' }
let(:email_domain) { 'gmail.com' }
let(:registered_email_address) { 'taken@' + email_domain }
let(:unregistered_email_address) { 'not_taken@' + email_domain }
let(:registered_and_confirmed_user) do
Expand Down Expand Up @@ -68,6 +68,32 @@
end
end

context 'email submission with special characters' do
context 'mx record are gmail' do
shared_examples 'blocked email address' do |email_address|
it 'sends the email with error code' do
user = create(*registered_and_confirmed_user)
user.suspend!

subject.submit(email: email_address, terms_accepted: '1')

expect_delivered_email_count(1)
expect_delivered_email(
to: [registered_email_address],
subject: t('user_mailer.suspended_create_account.subject'),
)
expect(subject.send(:blocked_email_address).user).to eq(user)
end
end
context 'when email contains a plus sign' do
it_behaves_like 'blocked email address', 'taken+1@gmail.com'
end
context 'when email contains a dot' do
it_behaves_like 'blocked email address', 'tak.en@gmail.com'
end
end
end

let(:variation_of_preexisting_email) { 'TAKEN@' + email_domain }
context 'when email is already taken' do
let!(:existing_user) { create(*registered_and_confirmed_user) }
Expand Down
44 changes: 44 additions & 0 deletions spec/models/suspended_email_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'rails_helper'

RSpec.describe SuspendedEmail, type: :model do
describe 'associations' do
it { should belong_to(:email_address).class_name('EmailAddress') }
end

describe 'validations' do
it { should validate_presence_of(:digested_base_email) }
end

describe '.generate_email_digest' do
it 'generates the correct digest for a given email' do
email = 'test@example.com'
expected_digest = Digest::SHA256.hexdigest('test@example.com')

expect(SuspendedEmail.generate_email_digest(email)).to eq(expected_digest)
end
end

describe '.blocked_email_address' do
context 'when the email is not blocked' do
it 'returns nil' do
email = 'not_blocked@example.com'

expect(SuspendedEmail.find_with_email(email)).to be_nil
end
end

context 'when the email is blocked' do
it 'returns the original email address' do
blocked_email = FactoryBot.create(:email_address, email: 'blocked@example.com')
digested_base_email = SuspendedEmail.generate_email_digest('blocked@example.com')
FactoryBot.create(
:suspended_email,
digested_base_email: digested_base_email,
email_address: blocked_email,
)

expect(SuspendedEmail.find_with_email('blocked@example.com')).to eq(blocked_email)
end
end
end
end
16 changes: 14 additions & 2 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@
end

describe 'user suspension' do
let(:user) { User.new }
let(:user) { create(:user) }
let(:cannot_reinstate_message) { :user_is_not_suspended }
let(:cannot_suspend_message) { :user_already_suspended }

Expand Down Expand Up @@ -768,6 +768,10 @@
UpdateUser.new(user: user, attributes: { unique_session_id: mock_session_id }).call
end

it 'creates SuspendedEmail records for each email address' do
expect { user.suspend! }.to(change { SuspendedEmail.count }.by(1))
end

it 'updates the suspended_at attribute with the current time' do
expect do
user.suspend!
Expand Down Expand Up @@ -822,9 +826,17 @@

describe '#reinstate!' do
before do
user.suspended_at = Time.zone.now
user.suspend!
user.reinstated_at = nil
end

it 'destroys SuspendedEmail records for each email address' do
email_address = user.email_addresses.last
expect { user.reinstate! }.
to(change { SuspendedEmail.find_with_email(email_address.email) }.
from(email_address).to(nil))
end

it 'updates the reinstated_at attribute with the current time' do
expect do
user.reinstate!
Expand Down