Skip to content

Add comprehensive email validation for blocked users#3

Open
everettbu wants to merge 1 commit into
blocked-email-validation-pre-copilotfrom
blocked-email-validation-post-copilot
Open

Add comprehensive email validation for blocked users#3
everettbu wants to merge 1 commit into
blocked-email-validation-pre-copilotfrom
blocked-email-validation-post-copilot

Conversation

@everettbu

Copy link
Copy Markdown

Test 3

… many times each email address is blocked, and last time it was blocked. Move email validation out of User model and into EmailValidator. Signup form remembers which email addresses have failed and shows validation error on email field.
Copilot AI review requested due to automatic review settings July 26, 2025 19:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a comprehensive email validation system for blocked users, adding a new BlockedEmail model and associated functionality to prevent user registration with blacklisted email addresses.

  • Introduces a new BlockedEmail model with database migration and validation logic
  • Refactors email validation from inline User model code to a reusable EmailValidator class
  • Adds client-side validation to prevent re-submission of rejected emails

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
app/models/blocked_email.rb New model for managing blocked email addresses with action types and tracking
lib/validators/email_validator.rb New validator class extracting email validation logic from User model
app/models/user.rb Refactored to use new EmailValidator and simplified validation declarations
db/migrate/20130724201552_create_blocked_emails.rb Database migration creating blocked_emails table with indexes
app/controllers/users_controller.rb Enhanced error response to include structured error data
app/assets/javascripts/discourse/controllers/create_account_controller.js Client-side validation to track and prevent resubmission of rejected emails
spec/models/blocked_email_spec.rb Test suite for BlockedEmail model functionality
spec/components/validators/email_validator_spec.rb Test suite for EmailValidator class
spec/fabricators/blocked_email_fabricator.rb Test factory for BlockedEmail objects
config/locales/server.en.yml Added localization for blocked email error message

Comment on lines +23 to +43
subject.should be_false
end

shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now)
end
end
end

context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { should be_true }
include_examples "when a BlockedEmail record matches"
end

context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { should be_false }

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newer RSpec expectation syntax expect(subject).to be_falsey instead of the deprecated should syntax.

Suggested change
subject.should be_false
end
shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now)
end
end
end
context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { should be_true }
include_examples "when a BlockedEmail record matches"
end
context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { should be_false }
expect(subject).to be_falsey
end
shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
expect(blocked_email.last_match_at).to be_within_one_second_of(Time.zone.now)
end
end
end
context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { expect(subject).to be_truthy }
include_examples "when a BlockedEmail record matches"
end
context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { expect(subject).to be_falsey }

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +43
subject.should be_false
end

shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now)
end
end
end

context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { should be_true }
include_examples "when a BlockedEmail record matches"
end

context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { should be_false }

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newer RSpec expectation syntax it { is_expected.to be_truthy } instead of the deprecated should syntax.

Suggested change
subject.should be_false
end
shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now)
end
end
end
context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { should be_true }
include_examples "when a BlockedEmail record matches"
end
context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { should be_false }
is_expected.to be_falsey
end
shared_examples "when a BlockedEmail record matches" do
it "updates statistics" do
Timecop.freeze(Time.zone.now) do
expect { subject }.to change { blocked_email.reload.match_count }.by(1)
expect(blocked_email.last_match_at).to be_within_one_second_of(Time.zone.now)
end
end
end
context "action_type is :block" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) }
it { is_expected.to be_truthy }
include_examples "when a BlockedEmail record matches"
end
context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { is_expected.to be_falsey }

Copilot uses AI. Check for mistakes.

context "action_type is :do_nothing" do
let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) }
it { should be_false }

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newer RSpec expectation syntax it { is_expected.to be_falsey } instead of the deprecated should syntax.

Suggested change
it { should be_false }
it { is_expected.to be_falsey }

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +19
record.errors[:email].should_not be_present
end

it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
record.errors[:email].should be_present

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newer RSpec expectation syntax expect(record.errors[:email]).not_to be_present instead of the deprecated should syntax.

Suggested change
record.errors[:email].should_not be_present
end
it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
record.errors[:email].should be_present
expect(record.errors[:email]).not_to be_present
end
it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
expect(record.errors[:email]).to be_present

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +19
record.errors[:email].should_not be_present
end

it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
record.errors[:email].should be_present

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newer RSpec expectation syntax expect(record.errors[:email]).to be_present instead of the deprecated should syntax.

Suggested change
record.errors[:email].should_not be_present
end
it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
record.errors[:email].should be_present
expect(record.errors[:email]).not_to be_present
end
it "adds an error when email matches a blocked email" do
BlockedEmail.stubs(:should_block?).with(record.email).returns(true)
validate
expect(record.errors[:email]).to be_present

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
record.match_count += 1
record.last_match_at = Time.zone.now
record.save

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The increment and save operations are not atomic, which could lead to race conditions in concurrent environments. Consider using increment!(:match_count) or update_counters for atomic counter updates.

Suggested change
record.match_count += 1
record.last_match_at = Time.zone.now
record.save
record.increment!(:match_count)
record.update(last_match_at: Time.zone.now)

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
record.match_count += 1
record.last_match_at = Time.zone.now
record.save

Copilot AI Jul 26, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting last_match_at separately from the counter increment creates a race condition. Consider using update_columns or combining both updates in a single atomic operation.

Suggested change
record.match_count += 1
record.last_match_at = Time.zone.now
record.save
record.update_columns(
match_count: record.match_count + 1,
last_match_at: Time.zone.now
)

Copilot uses AI. Check for mistakes.
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has been open for 60 days with no activity. To keep it open, remove the stale tag, push code, or add a comment. Otherwise, it will be closed in 14 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants