From c9daa530ff51562ea08d040b1f77f8a20be03e43 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 11 Aug 2025 18:00:06 -0230 Subject: [PATCH] Refine invitation email validation and expiration scopes --- .../better_together/platform_invitation.rb | 12 +++++----- .../platform_invitation_spec.rb | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/models/better_together/platform_invitation.rb b/app/models/better_together/platform_invitation.rb index f16d8b264..f99f36f88 100644 --- a/app/models/better_together/platform_invitation.rb +++ b/app/models/better_together/platform_invitation.rb @@ -32,8 +32,10 @@ class PlatformInvitation < ApplicationRecord has_rich_text :greeting, encrypted: true - validates :invitee_email, uniqueness: { scope: :invitable_id, allow_nil: true } - validates :invitee_email, uniqueness: { scope: :invitable_id, allow_nil: true, allow_blank: true } + validates :invitee_email, + uniqueness: { scope: :invitable_id, case_sensitive: false }, + format: { with: URI::MailTo::EMAIL_REGEXP }, + allow_blank: true validates :locale, presence: true, inclusion: { in: I18n.available_locales.map(&:to_s) } validates :status, presence: true, inclusion: { in: STATUS_VALUES.values } validates :token, uniqueness: true @@ -46,10 +48,8 @@ class PlatformInvitation < ApplicationRecord scope :pending, -> { where(status: STATUS_VALUES[:pending]) } scope :accepted, -> { where(status: STATUS_VALUES[:accepted]) } - # TODO: Check expired scope to ensure that it includes those wit no value for valid_until - scope :expired, -> { where('valid_until < ?', Time.current) } - - # TODO: add 'not expired' scope to find only invitations that are available + scope :expired, -> { where('valid_until IS NULL OR valid_until < ?', Time.current) } + scope :not_expired, -> { where('valid_until >= ?', Time.current) } def self.load_all_subclasses Rails.application.eager_load! # Ensure all models are loaded diff --git a/spec/models/better_together/platform_invitation_spec.rb b/spec/models/better_together/platform_invitation_spec.rb index 5e579e70c..d62643ede 100644 --- a/spec/models/better_together/platform_invitation_spec.rb +++ b/spec/models/better_together/platform_invitation_spec.rb @@ -25,8 +25,10 @@ module BetterTogether # rubocop:todo Metrics/ModuleLength describe 'ActiveModel validations' do it { is_expected.to validate_uniqueness_of(:invitee_email).scoped_to(:invitable_id) - .allow_nil.allow_blank.case_insensitive + .allow_blank.case_insensitive } + it { is_expected.to allow_value('test@example.com').for(:invitee_email) } + it { is_expected.not_to allow_value('invalid_email').for(:invitee_email) } it { is_expected.to validate_presence_of(:locale) } it { is_expected.to validate_inclusion_of(:locale).in_array(I18n.available_locales.map(&:to_s)) } it { is_expected.to validate_presence_of(:status) } @@ -86,12 +88,22 @@ module BetterTogether # rubocop:todo Metrics/ModuleLength end describe '.expired' do - it 'returns only expired invitations' do - expired_invitation = create(:better_together_platform_invitation, valid_until: 1.day.ago) + it 'includes invitations with nil or past valid_until' do + nil_invitation = create(:better_together_platform_invitation, valid_until: nil) + past_invitation = create(:better_together_platform_invitation, valid_until: 1.day.ago) create(:better_together_platform_invitation, valid_until: 1.day.from_now) - expect(BetterTogether::PlatformInvitation.expired).to include(expired_invitation) - expect(BetterTogether::PlatformInvitation.expired.count).to eq(1) + expect(described_class.expired).to match_array([nil_invitation, past_invitation]) + end + end + + describe '.not_expired' do + it 'includes only invitations with future valid_until' do + create(:better_together_platform_invitation, valid_until: nil) + create(:better_together_platform_invitation, valid_until: 1.day.ago) + future_invitation = create(:better_together_platform_invitation, valid_until: 1.day.from_now) + + expect(described_class.not_expired).to match_array([future_invitation]) end end end