diff --git a/app/models/concerns/user/confirmation.rb b/app/models/concerns/user/confirmation.rb new file mode 100644 index 00000000000..46fdb0210a5 --- /dev/null +++ b/app/models/concerns/user/confirmation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module User::Confirmation + extend ActiveSupport::Concern + + included do + scope :confirmed, -> { where.not(confirmed_at: nil) } + scope :unconfirmed, -> { where(confirmed_at: nil) } + + def confirm + wrap_email_confirmation { super } + end + end + + def confirmed? + confirmed_at.present? + end + + def unconfirmed? + !confirmed? + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 2f3640b62a6..06a28761190 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -58,6 +58,7 @@ class User < ApplicationRecord include LanguagesHelper include Redisable + include User::Confirmation include User::HasSettings include User::LdapAuthenticable include User::Omniauthable @@ -118,8 +119,6 @@ class User < ApplicationRecord scope :recent, -> { order(id: :desc) } scope :pending, -> { where(approved: false) } scope :approved, -> { where(approved: true) } - scope :confirmed, -> { where.not(confirmed_at: nil) } - scope :unconfirmed, -> { where(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } scope :active, -> { confirmed.signed_in_recently.account_not_suspended } @@ -184,10 +183,6 @@ class User < ApplicationRecord current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago end - def confirmed? - confirmed_at.present? - end - def invited? invite_id.present? end @@ -212,12 +207,6 @@ class User < ApplicationRecord account_id end - def confirm - wrap_email_confirmation do - super - end - end - # Mark current email as confirmed, bypassing Devise def mark_email_as_confirmed! wrap_email_confirmation do @@ -264,10 +253,6 @@ class User < ApplicationRecord confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial? end - def unconfirmed? - !confirmed? - end - def unconfirmed_or_pending? unconfirmed? || pending? end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c71b7a600ba..438937b1001 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,6 +10,7 @@ RSpec.describe User do let(:account) { Fabricate(:account, username: 'alice') } it_behaves_like 'two_factor_backupable' + it_behaves_like 'User::Confirmation' describe 'otp_secret' do it 'encrypts the saved value' do @@ -65,14 +66,6 @@ RSpec.describe User do end end - describe 'confirmed' do - it 'returns an array of users who are confirmed' do - Fabricate(:user, confirmed_at: nil) - confirmed_user = Fabricate(:user, confirmed_at: Time.zone.now) - expect(described_class.confirmed).to contain_exactly(confirmed_user) - end - end - describe 'signed_in_recently' do it 'returns a relation of users who have signed in during the recent period' do recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) @@ -228,79 +221,6 @@ RSpec.describe User do end end - describe '#confirmed?' do - it 'returns true when a confirmed_at is set' do - user = Fabricate.build(:user, confirmed_at: Time.now.utc) - expect(user.confirmed?).to be true - end - - it 'returns false if a confirmed_at is nil' do - user = Fabricate.build(:user, confirmed_at: nil) - expect(user.confirmed?).to be false - end - end - - describe '#confirm' do - subject { user.confirm } - - let(:new_email) { 'new-email@example.com' } - - before do - allow(TriggerWebhookWorker).to receive(:perform_async) - end - - context 'when the user is already confirmed' do - let!(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: true, unconfirmed_email: new_email) } - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - - context 'when the user is a new user' do - let(:user) { Fabricate(:user, confirmed_at: nil, unconfirmed_email: new_email) } - - context 'when the user is already approved' do - before do - Setting.registrations_mode = 'approved' - user.approve! - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user does not require explicit approval' do - before do - Setting.registrations_mode = 'open' - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user requires explicit approval but is not approved' do - before do - Setting.registrations_mode = 'approved' - end - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - end - end - describe '#approve!' do subject { user.approve! } diff --git a/spec/support/examples/models/concerns/user/confirmation.rb b/spec/support/examples/models/concerns/user/confirmation.rb new file mode 100644 index 00000000000..4edc402f950 --- /dev/null +++ b/spec/support/examples/models/concerns/user/confirmation.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.shared_examples 'User::Confirmation' do + describe 'Scopes' do + let!(:unconfirmed_user) { Fabricate :user, confirmed_at: nil } + let!(:confirmed_user) { Fabricate :user, confirmed_at: Time.now.utc } + + describe '.confirmed' do + it 'returns users who are confirmed' do + expect(described_class.confirmed) + .to contain_exactly(confirmed_user) + end + end + + describe '.unconfirmed' do + it 'returns users who are not confirmed' do + expect(described_class.unconfirmed) + .to contain_exactly(unconfirmed_user) + end + end + end + + describe '#confirmed?' do + subject { Fabricate.build(:user, confirmed_at:) } + + context 'when confirmed_at is set' do + let(:confirmed_at) { Time.now.utc } + + it { is_expected.to be_confirmed } + end + + context 'when confirmed_at is not set' do + let(:confirmed_at) { nil } + + it { is_expected.to_not be_confirmed } + end + end + + describe '#unconfirmed?' do + subject { Fabricate.build(:user, confirmed_at:) } + + context 'when confirmed_at is set' do + let(:confirmed_at) { Time.now.utc } + + it { is_expected.to_not be_unconfirmed } + end + + context 'when confirmed_at is not set' do + let(:confirmed_at) { nil } + + it { is_expected.to be_unconfirmed } + end + end + + describe '#confirm' do + subject { user.confirm } + + let(:new_email) { 'new-email@host.example' } + + before { allow(TriggerWebhookWorker).to receive(:perform_async) } + + context 'when the user is already confirmed' do + let!(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: true, unconfirmed_email: new_email) } + + it 'sets email to unconfirmed_email and does not trigger web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) + end + end + + context 'when the user is a new user' do + let(:user) { Fabricate(:user, confirmed_at: nil, unconfirmed_email: new_email) } + + context 'when the user does not require explicit approval' do + before { Setting.registrations_mode = 'open' } + + it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once + end + end + + context 'when registrations mode is approved' do + before { Setting.registrations_mode = 'approved' } + + context 'when the user is already approved' do + before { user.approve! } + + it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once + end + end + + context 'when the user is not approved' do + it 'sets email to unconfirmed_email and does not trigger web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) + end + end + end + end + end +end