Skip to content

Add comprehensive email validation for blocked users#3

Open
ShashankFC wants to merge 1 commit into
blocked-email-validation-prefrom
blocked-email-validation-post
Open

Add comprehensive email validation for blocked users#3
ShashankFC wants to merge 1 commit into
blocked-email-validation-prefrom
blocked-email-validation-post

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 3

Summary by CodeRabbit

Release Notes

New Features

  • Added email blocking mechanism for account creation that prevents specific email addresses from being used
  • Enhanced email validation to track and reject previously failed emails
  • Improved account creation error responses with detailed validation information

Database

  • New database table added to persist blocked email configurations and tracking data

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/discourse-coderabbit#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.
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 13, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change introduces a comprehensive email blocking system for account creation. A new BlockedEmail model with validation logic tracks rejected emails, which are persisted and checked during account creation. The account creation flow now returns additional error details, and old email validation methods have been consolidated into a new EmailValidator class with whitelist/blacklist support.

Changes

Cohort / File(s) Summary
Account Creation Flow
app/assets/javascripts/discourse/controllers/create_account_controller.js, app/controllers/users_controller.rb
JavaScript controller now tracks rejected emails in an Ember array and re-validates on list changes. Rails controller extended to return email errors and values in error responses for client-side tracking.
Email Blocking System
app/models/blocked_email.rb, lib/validators/email_validator.rb, config/locales/server.en.yml
New BlockedEmail model with match tracking, action types, and should_block? logic. New EmailValidator checks whitelists, blacklists, and BlockedEmail records. Translation added for blocked email error message.
User Model Consolidation
app/models/user.rb
Removed legacy email_validator and email_in_restriction_setting? methods. Consolidated email validations (presence, uniqueness, format) into standard Rails validator declarations.
Database Schema
db/migrate/20130724201552_create_blocked_emails.rb
Migration creates blocked_emails table with email, action_type, match_count, last_match_at, and timestamps. Unique index on email column.
Tests & Fixtures
spec/components/validators/email_validator_spec.rb, spec/fabricators/blocked_email_fabricator.rb, spec/models/blocked_email_spec.rb
Comprehensive specs for EmailValidator blocked email handling, BlockedEmail model defaults and statistics, and a fabricator for test fixtures.

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser as Browser / Client
    participant Server as Rails Server
    participant DB as Database
    
    User->>Browser: Submit account creation form
    Browser->>Browser: Validate email against rejectedEmails
    alt Email in rejected list
        Browser->>User: Show validation error
    else Email not rejected
        Browser->>Server: POST /users (with email)
        Server->>DB: Check BlockedEmail.should_block?(email)
        DB->>DB: Find record, increment match_count
        DB-->>Server: Return blocking decision
        alt Should block
            Server-->>Browser: Error response (email in values)
            Browser->>Browser: Add email to rejectedEmails
            Browser->>User: Show blocked email error
        else Allow creation
            Server->>DB: Create user record
            DB-->>Server: Success
            Server-->>Browser: Success response
            Browser->>User: Account created
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A fluffy rabbit hops with glee,
Blocked emails now can't cause a spree!
The system tracks each naughty sender,
With match counts growing, growing tender.
🐰✉️ Validation made just right and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the changeset—implementing comprehensive email validation for blocked users through a new BlockedEmail model and EmailValidator.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @app/assets/javascripts/discourse/controllers/create_account_controller.js:
- Around line 274-276: The check that pushes to
createAccountController.get('rejectedEmails') can still push undefined because
it only verifies result.values exists; update the conditional to ensure
result.values.email is present and non-null/undefined before calling pushObject
(e.g. change the if to check result.values && result.values.email or use
optional chaining/result.values?.email), and only push result.values.email when
that value is truthy to avoid inserting undefined into rejectedEmails.
- Line 17: CreateAccountController's rejectedEmails array persists across modal
sessions; to fix, clear it when the modal opens or when the form is reset by
calling this.rejectedEmails.clear() (or reassigning Em.A([])) inside the
modal-open/reset handler rather than only at initialization. Add that clear call
to the controller method that runs when the modal is shown (e.g., the modal
open/show handler or the controller's resetForm/initializeForm routine) so
previously rejected addresses don't block subsequent submissions; also consider
clearing it just before running the email validation logic to ensure fresh
state.

In @app/models/blocked_email.rb:
- Around line 11-19: In should_block?(email) on BlockedEmail, perform a
case-insensitive lookup by downcasing the incoming email (e.g., use
Email.downcase(email) or email.downcase) when querying instead of where(email:
email), increment match_count and update last_match_at as before but replace
record.save with record.save! wrapped in a begin/rescue to catch
ActiveRecord::RecordInvalid (or StandardError) and log the failure so statistics
updates aren’t silently lost; keep the final return checking record &&
record.action_type == actions[:block].

In @lib/validators/email_validator.rb:
- Around line 13-15: Validation currently calls BlockedEmail.should_block?,
which has side effects (increments match_count and sets last_match_at); change
the validator (email_validator.rb) to call a new side-effect-free method like
BlockedEmail.blocked? (or similar) that performs only the check, and implement
that method in BlockedEmail model reusing the same matching logic but without
updating match_count/last_match_at; reserve should_block? (which updates stats)
to be called only during real signup attempts (e.g., in the signup/create flow)
so that repeated validations do not inflate statistics; update/add tests to
cover both the pure check method and the stats-updating method.
🧹 Nitpick comments (3)
spec/components/validators/email_validator_spec.rb (2)

3-23: Consider removing extra empty lines for consistency with RuboCop conventions.

The test logic is correct and properly covers the blocked email validation scenarios. However, static analysis flagged extra empty lines at the block body beginning (after line 4) and end (before line 23).

♻️ Optional: Remove extra empty lines
 describe EmailValidator do
-
   let(:record) { Fabricate.build(:user, email: "bad@spamclub.com") }
   let(:validator) { described_class.new({attributes: :email}) }
   subject(:validate) { validator.validate_each(record,:email,record.email) }

   context "blocked email" do
     it "doesn't add an error when email doesn't match a blocked email" do
       BlockedEmail.stubs(:should_block?).with(record.email).returns(false)
       validate
       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
     end
   end
-
 end

9-21: Consider adding test coverage for whitelist/blacklist interaction.

The blocked email tests are well-structured, but the EmailValidator also checks whitelist and blacklist settings before checking BlockedEmail.should_block?. The validator only calls should_block? when there are no prior errors. Consider adding tests to verify blocked email checks are skipped when whitelist/blacklist validation already failed.

spec/models/blocked_email_spec.rb (1)

9-9: Consider updating to modern RSpec syntax.

The tests use the deprecated should syntax (e.g., should ==, should be_true, should be_false). While this works, modern RSpec prefers expect(...).to:

  • should ==expect(...).to eq(...)
  • should be_trueexpect(...).to be true
  • should be_falseexpect(...).to be false
  • should be_nilexpect(...).to be_nil

This is a minor style consistency issue and can be addressed later if the codebase already uses the older syntax elsewhere.

Also applies to: 15-15, 23-23, 30-30, 37-37, 43-43

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e25638d and 5f8a130.

📒 Files selected for processing (10)
  • app/assets/javascripts/discourse/controllers/create_account_controller.js
  • app/controllers/users_controller.rb
  • app/models/blocked_email.rb
  • app/models/user.rb
  • config/locales/server.en.yml
  • db/migrate/20130724201552_create_blocked_emails.rb
  • lib/validators/email_validator.rb
  • spec/components/validators/email_validator_spec.rb
  • spec/fabricators/blocked_email_fabricator.rb
  • spec/models/blocked_email_spec.rb
🧰 Additional context used
🧬 Code graph analysis (4)
spec/fabricators/blocked_email_fabricator.rb (1)
app/models/blocked_email.rb (1)
  • actions (7-9)
lib/validators/email_validator.rb (1)
app/models/blocked_email.rb (1)
  • should_block? (11-19)
spec/components/validators/email_validator_spec.rb (1)
lib/validators/email_validator.rb (2)
  • validate_each (1-24)
  • validate_each (3-16)
spec/models/blocked_email_spec.rb (1)
app/models/blocked_email.rb (2)
  • actions (7-9)
  • should_block? (11-19)
🪛 RuboCop (1.82.1)
spec/components/validators/email_validator_spec.rb

[convention] 4-5: Extra empty line detected at block body beginning.

(Layout/EmptyLinesAroundBlockBody)


[convention] 22-23: Extra empty line detected at block body end.

(Layout/EmptyLinesAroundBlockBody)

spec/models/blocked_email_spec.rb

[convention] 4-5: Extra empty line detected at block body beginning.

(Layout/EmptyLinesAroundBlockBody)


[convention] 47-48: Extra empty line detected at block body end.

(Layout/EmptyLinesAroundBlockBody)

🔇 Additional comments (10)
db/migrate/20130724201552_create_blocked_emails.rb (1)

1-12: LGTM!

The migration correctly creates the blocked_emails table with appropriate columns and constraints. The unique index on email ensures efficient lookups and prevents duplicate entries.

config/locales/server.en.yml (1)

734-734: LGTM!

The new translation key blocked is correctly placed under en.user.email and aligns with the I18n.t(:'user.email.blocked') call in EmailValidator.

spec/fabricators/blocked_email_fabricator.rb (1)

1-4: LGTM!

The fabricator correctly generates unique emails using a sequence and sets the default action_type to :block via BlockedEmail.actions[:block], consistent with the model's enum definition.

app/controllers/users_controller.rb (1)

195-200: LGTM!

The enhanced error response structure correctly provides:

  1. errors as a hash for field-specific error handling
  2. values containing user-submitted input to enable client-side persistence of rejected emails

This aligns with the client-side rejectedEmails handling mentioned in the PR objectives.

app/assets/javascripts/discourse/controllers/create_account_controller.js (1)

68-74: Email rejection validation logic looks correct.

The validation properly checks if the current email is in the rejectedEmails array and returns an appropriate validation failure. The dependency on rejectedEmails.@each ensures the computed property re-evaluates when the array contents change.

Also applies to: 96-96

lib/validators/email_validator.rb (2)

1-16: Overall validation structure looks good.

The validator correctly handles:

  • Whitelist mode (only allow specific domains)
  • Blacklist mode (deny specific domains)
  • Blocked email check (per-email blocking)

The precedence is also correct: whitelist takes priority over blacklist.


18-22: The domain parsing implementation is correct for the actual format used in the codebase. Tests confirm that email_domains_whitelist and email_domains_blacklist settings use only pipe character (|) as a delimiter for multiple domains, not commas, spaces, or newlines. The existing gsub('.', '\.') correctly escapes literal periods to prevent wildcard matching. No changes needed.

Likely an incorrect or invalid review comment.

app/models/user.rb (1)

46-47: Good refactoring of email validation.

Consolidating email validation into two clear lines improves maintainability:

  • Line 46: Handles presence and uniqueness (database-level checks)
  • Line 47: Delegates domain/blocking validation to the new EmailValidator class, only when email changes

The if: :email_changed? guard is appropriate to avoid unnecessary validation on updates that don't touch the email field.

app/models/blocked_email.rb (1)

1-9: Model structure and defaults look good.

The model correctly:

  • Uses before_validation to set defaults before validation runs
  • Validates email presence and uniqueness
  • Provides a type-safe Enum for action types via Enum.new(:block, :do_nothing)

Also applies to: 21-23

spec/models/blocked_email_spec.rb (1)

26-33: Good use of shared examples for DRY test code.

The shared example "when a BlockedEmail record matches" effectively reduces duplication between the :block and :do_nothing action type contexts. The custom matcher be_within_one_second_of is properly defined and used correctly.

accountPasswordConfirm: 0,
accountChallenge: 0,
formSubmitted: false,
rejectedEmails: Em.A([]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "CreateAccountController" --type js -C 3

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 1444


🏁 Script executed:

# Also check the file itself to see the full context
wc -l app/assets/javascripts/discourse/controllers/create_account_controller.js

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 162


🏁 Script executed:

# Check if this is a modal controller and how it's instantiated
rg -n "ModalFunctionality|new.*CreateAccount|CreateAccountController" --type js | head -30

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 4140


🏁 Script executed:

# Look for similar controller patterns with mutable defaults in the same directory
fd -e js . app/assets/javascripts/discourse/controllers/ | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 "{}"'

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 4152


🏁 Script executed:

cat -n app/assets/javascripts/discourse/controllers/create_account_controller.js | head -100

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 3734


🏁 Script executed:

# Check if there are any methods that clear or reset rejectedEmails
rg -n "rejectedEmails" app/assets/javascripts/discourse/controllers/create_account_controller.js

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 322


🏁 Script executed:

# Check for init or reset methods in the controller
rg -n "init:|reset:|onOpen:|onShow:" app/assets/javascripts/discourse/controllers/create_account_controller.js

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 67


🏁 Script executed:

# Look at ModalFunctionality to see if it handles cleanup
cat -n app/assets/javascripts/discourse/mixins/modal_functionality.js

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 852


🏁 Script executed:

cat -n app/assets/javascripts/discourse/controllers/create_account_controller.js | tail -200

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 8447


🏁 Script executed:

# Specifically look for any clearing/reset logic around rejectedEmails
rg -n -B5 -A5 "rejectedEmails.*pushObject|pushObject.*rejectedEmails" app/assets/javascripts/discourse/controllers/create_account_controller.js

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 657


Rejected emails array should be cleared when the modal is opened, not when initialized.

The rejectedEmails array is populated when email validation fails (line 275) but is never cleared. When the modal is closed and reopened, previously rejected emails remain in the array and continue to block form submission. This causes state pollution across modal sessions.

Since CreateAccountController is a singleton modal controller, the issue is not multiple instances sharing state, but rather persistent state across separate modal interactions. Clear the array when the modal opens or when form inputs are reset.

🤖 Prompt for AI Agents
In @app/assets/javascripts/discourse/controllers/create_account_controller.js at
line 17, CreateAccountController's rejectedEmails array persists across modal
sessions; to fix, clear it when the modal opens or when the form is reset by
calling this.rejectedEmails.clear() (or reassigning Em.A([])) inside the
modal-open/reset handler rather than only at initialization. Add that clear call
to the controller method that runs when the modal is shown (e.g., the modal
open/show handler or the controller's resetForm/initializeForm routine) so
previously rejected addresses don't block subsequent submissions; also consider
clearing it just before running the email validation logic to ensure fresh
state.

Comment on lines +274 to +276
if (result.errors && result.errors.email && result.values) {
createAccountController.get('rejectedEmails').pushObject(result.values.email);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Verify null safety when accessing nested properties.

The condition checks result.errors && result.errors.email && result.values, but then accesses result.values.email. If result.values exists but result.values.email is undefined/null, pushObject would add an undefined value to the array.

Suggested fix
-        if (result.errors && result.errors.email && result.values) {
+        if (result.errors && result.errors.email && result.values && result.values.email) {
           createAccountController.get('rejectedEmails').pushObject(result.values.email);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result.errors && result.errors.email && result.values) {
createAccountController.get('rejectedEmails').pushObject(result.values.email);
}
if (result.errors && result.errors.email && result.values && result.values.email) {
createAccountController.get('rejectedEmails').pushObject(result.values.email);
}
🤖 Prompt for AI Agents
In @app/assets/javascripts/discourse/controllers/create_account_controller.js
around lines 274 - 276, The check that pushes to
createAccountController.get('rejectedEmails') can still push undefined because
it only verifies result.values exists; update the conditional to ensure
result.values.email is present and non-null/undefined before calling pushObject
(e.g. change the if to check result.values && result.values.email or use
optional chaining/result.values?.email), and only push result.values.email when
that value is truthy to avoid inserting undefined into rejectedEmails.

Comment on lines +11 to +19
def self.should_block?(email)
record = BlockedEmail.where(email: email).first
if record
record.match_count += 1
record.last_match_at = Time.zone.now
record.save
end
record && record.action_type == actions[:block]
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "blocked_email.rb" -type f

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 114


🏁 Script executed:

cat -n ./app/models/blocked_email.rb

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 802


🏁 Script executed:

# Check for email normalization patterns in the codebase
rg -n "downcase.*email|email.*downcase|email.*lower|lower.*email" --type ruby -C 2

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 7078


🏁 Script executed:

# Check if BlockedEmail is created/updated elsewhere with email normalization
rg -n "BlockedEmail" --type ruby -B 3 -A 3

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 6419


🏁 Script executed:

# Check how the email validator is used and if emails are normalized before validation
rg -n "email_validator|EmailValidator" --type ruby -B 5 -A 5

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 1909


🏁 Script executed:

# Check how User model handles email normalization during creation/validation
sed -n '1,300p' ./app/models/user.rb | cat -n

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 10703


🏁 Script executed:

# Check if User model normalizes email on assignment or before_save
rg -n "email.*=" ./app/models/user.rb | head -20

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 487


🏁 Script executed:

# Check for before_save and email normalization in User model
rg -n "before_save|def email" ./app/models/user.rb -A 5

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 774


🏁 Script executed:

# Verify the Email.downcase utility and confirm it handles case-insensitive matching properly
cat -n lib/email.rb

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 942


🏁 Script executed:

# Check if there's any test that validates case-insensitive email matching for BlockedEmail
cat -n spec/models/blocked_email_spec.rb

Repository: AI-Code-Review-Evals/coderabbit-discourse

Length of output: 1984


Address case-insensitive email matching and silent save failure in should_block?

Two issues in the should_block? method:

  1. Case-sensitive email lookup (Line 12): The query where(email: email) is case-sensitive. A blocked email stored as "bad@spam.com" won't match lookups for "Bad@Spam.com", allowing users to bypass the block by varying case. The codebase establishes a pattern of using Email.downcase() for case-insensitive email handling; apply it here.

  2. Silent save failure (Line 16): record.save fails silently if validation errors occur. Statistics updates (match_count, last_match_at) are lost without indication. Use record.save! and handle exceptions to ensure failures are logged.

Suggested improvements
 def self.should_block?(email)
-  record = BlockedEmail.where(email: email).first
+  record = BlockedEmail.where("lower(email) = ?", email.to_s.downcase).first
   if record
     record.match_count += 1
     record.last_match_at = Time.zone.now
-    record.save
+    record.save!
   end
   record && record.action_type == actions[:block]
+rescue ActiveRecord::RecordInvalid => e
+  Rails.logger.error("Failed to update BlockedEmail stats: #{e.message}")
+  record && record.action_type == actions[:block]
 end
🤖 Prompt for AI Agents
In @app/models/blocked_email.rb around lines 11 - 19, In should_block?(email) on
BlockedEmail, perform a case-insensitive lookup by downcasing the incoming email
(e.g., use Email.downcase(email) or email.downcase) when querying instead of
where(email: email), increment match_count and update last_match_at as before
but replace record.save with record.save! wrapped in a begin/rescue to catch
ActiveRecord::RecordInvalid (or StandardError) and log the failure so statistics
updates aren’t silently lost; keep the final return checking record &&
record.action_type == actions[:block].

Comment on lines +13 to +15
if record.errors[attribute].blank? and BlockedEmail.should_block?(value)
record.errors.add(attribute, I18n.t(:'user.email.blocked'))
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

BlockedEmail.should_block? has a side effect during validation.

Calling should_block? increments match_count and updates last_match_at (as seen in app/models/blocked_email.rb lines 14-16). This means statistics are updated every time validation runs, not just on actual signup attempts. If a user corrects other form fields and resubmits, the blocked email counter would be inflated.

Consider whether this is the intended behavior. If not, you may want a separate method that only checks without updating statistics, reserving the stat update for actual signup attempts.

🤖 Prompt for AI Agents
In @lib/validators/email_validator.rb around lines 13 - 15, Validation currently
calls BlockedEmail.should_block?, which has side effects (increments match_count
and sets last_match_at); change the validator (email_validator.rb) to call a new
side-effect-free method like BlockedEmail.blocked? (or similar) that performs
only the check, and implement that method in BlockedEmail model reusing the same
matching logic but without updating match_count/last_match_at; reserve
should_block? (which updates stats) to be called only during real signup
attempts (e.g., in the signup/create flow) so that repeated validations do not
inflate statistics; update/add tests to cover both the pure check method and the
stats-updating method.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants