email and new_email are two distinct columns. Every email should be unique, so if an email is added into either column it cannot already exist in either email or new_email columns.
Back story: I create the primary email for activated accounts and have a second new_email for when a user decides to change their email address but has not yet validated the new one via an email confirmation.
Most SO searches gave the scope solution:
I've tried validates :email, uniqueness: {scope: :new_email} and validates :new_email, uniqueness: {scope: :email} however I'm pretty sure this functionality acts to create a new key among the email1, email2 pair which is not the desired effect.
Currently I'm judging the validity of my code with the following two test cases (which are failing)
test "new_email should not match old email" do
#user.new_email = #user.email
assert_not #user.valid?
end
test "new_email addresses should be unique" do
duplicate_user = #user.dup
duplicate_user.new_email = #user.email.upcase
duplicate_user.email = #user.email + "z"
#user.save
assert_not duplicate_user.valid?
end
Scope's not going to cut it... it's really there to make sure that mail is unique amongst all records that might share the same new_mail and won't catch uniqueness for different new_email values, nor will it compare the values across the two columns.
Use standard 'uniqueness' to ensure there's no duplication within the column.
Then you'll need to create a custom validation for cross column...
validate :emails_unique
def emails_unique
found_user = User.find_by(new_email: email)
errors.add(:email, "email is someone's new email") if found_user
return unless new_email
found_user = User.find_by(email: new_email)
errors.add(:new_email, "new email is someone's regular email") if found_user
end
I think you really just want to write a custom validator, something like this:
validate :emails_are_unique
def emails_are_unique
if email == new_email
errors.add(:base, 'Your new email can not match your old email')
end
end
Which will do what you are looking for.
validates :email, uniqueness: {scope: :new_email} will ensure that email is unique among all records with the same value for new_email. This is not what you want.
You'll have to write a custom validation function, e.g.:
def validate_email_and_new_email_unique
if email && self.class.exists?("email = :email OR new_email = :email", email: email)
errors.add :email, "must be unique"
end
if new_email && self.class.exists?("email = :email OR new_email = :email", email: new_email)
errors.add :new_email, "must be unique"
end
end
Related
I have a restriction on my Rails DB to force unique usernames, and I create a user at the start of each of my model tests. I'm trying to create a second user with the same username as the first and I expect this to not be valid, but it is returning as valid.
I've tried tweaking the code to use the new, save, and create methods when generating new users but with no success.
Registration Model:
class Registration < ApplicationRecord
#username_length = (3..20)
#password_requirements = /\A
(?=.{8,}) # Password must be at least 8 characters
(?=.*\d) # Password must contain at least one number
(?=.*[a-z]) # Password must contain at least one lowercase letter
(?=.*[A-Z]) # Password must contain at least one capital letter
# Password must have special character
(?=.*[['!', '#', '#', '$', '%', '^', '&']])
/x
validates :username, length: #username_length, uniqueness: true
validates :password, format: #password_requirements
validates :email, uniqueness: true
has_secure_password
has_secure_token :auth_token
def invalidate_token
self.update_columns(auth_token: nil)
end
def self.validate_login(username, password)
user = Registration.find_by(username: username)
if user && user.authenticate(password)
user
end
end
end
Registration Tests:
require 'rails_helper'
RSpec.describe Registration, type: :model do
before do
#user = Registration.new(
username: '1234',
password: 'abcdeF7#',
email: 'test#test.com',
name: 'One'
)
end
it 'should not be valid if the username is already taken' do
#user.save!(username: '1234')
expect(#user).not_to be_valid
end
end
I would expect this test to pass due to it being a duplicate username.
As fabio said, you dont have second Registration object to check uniquness.
You just checked your saved #user is valid or not which is always valid and saved in DB. To check your uniqueness validation you can do something like this -
RSpec.describe Registration, type: :model do
before do
#user = Registration.create(
username: '1234',
password: 'abcdeF7#',
email: 'test#test.com',
name: 'One'
)
#invalid_user = #user.dup
#invalid_user.email = "test1#test.com"
end
it 'should not be valid if the username is already taken' do
expect(#invalid_user.valid?).should be_falsey
end
end
#user.save! will raise an error even before reaching the expect as mentioned in comments by Fabio
Also, if it is important to you to test db level constraint you can do:
expect { #user.save validate: false }.to raise_error(ActiveRecord::RecordNotUnique)
I am working through the Ruby on Rails Tutorial by Michael Hartl and have generated an interesting dilemma. I will have done something wrong, so I need your help finding the issue.
The issue surrounds the validation of a password property within a User model. The initial validation of this property was:
validates :password, presence: true,
confirmation: true,
length: { minimum: 6 }
This requires a minimum length of the password and is designed to satisfy the situation where a new user creates their instance.
I have created the following tests (and I wish I had used Rspec!) guided by the book. These tests check that the validations work:
test "password must not be blank or made up of spaces" do
#user.password = #user.password_confirmation = " "
assert_not #user.valid?
end
test "password must not be empty/nil" do
#user.password = #user.password_confirmation = ""
assert_not #user.valid?
end
So, we’re checking that the password field cannot contain either a space, or a nil entry. With the current validations, these tests pass. All is well.
I have progressed to allowing a user to edit their profile. This enables the user to change their name, email address and password/confirmation if they choose. In order to allow a user not to change their password if they don’t want to, additional validation is added to the password property of the model, adding allow_blank: true such as:
validates :password, presence: true,
confirmation: true,
length: { minimum: 6 },
allow_blank: true # added this!
So, the user can now leave the two password fields blank when they edit their profile if they don’t want to change their profile. This satisfies the test:
test "successful edit" do
log_in_as #user
get edit_user_path(#user)
assert_template 'users/edit'
name = "Foo Bar"
email = "foo#valid.co.uk"
patch user_path(#user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to #user
#user.reload
assert_equal #user.name, name
assert_equal #user.email, email
end
This enables a user to edit just their name & email and, by leaving their two password fields blank, there’s no need to change, or re-enter, their password. This throws a FAIL on a long passing test, as above, such as:
test "password must not be blank or made up of spaces" do
#user.password = #user.password_confirmation = " "
assert_not #user.valid?
end
The test fails because the user is validated. The slightly different test, which tests for nil, not blank, passes:
test "password must not be empty/nil" do
#user.password = #user.password_confirmation = ""
assert_not #user.valid?
end
So a password of “” is caught but a password of “ “ works fine for creating a new user or editing an existing user.
Adding allow_blank: true to the user model validation of password seems to have caused this. So, I am stuck between two tests failing. If I omit allow_blank: true, this test fails (full test pasted above):
test "successful edit" do
.
.
patch user_path(#user), params: { user:
{ name: name,
email: email,
password: "",
password_confirmation: "" } }
.
assert_equal #user.name, name
assert_equal #user.email, email
end
Sending the blank password and password_confirmation fails the test as it isn’t allowed to be blank.
Adding allow_blank: true within the validation fails this test:
test "password must not be blank or made up of spaces" do
#user.password = #user.password_confirmation = " "
assert_not #user.valid?
end
This fail allows a user to be created with a password consisting of spaces. A nil password, i.e. no characters at all, is not allowed. That test works.
This leaves me in the position where I must decide between a user having to change/repeat their two password fields if they edit their profile, OR, allowing a scenario where a user can sign up with a password consisting of one space, or many spaces, as this test doesn’t throw the expected failure message:
test "password must not be blank or made up of spaces" do
#user.password = #user.password_confirmation = " "
assert_not #user.valid?
end
The addition of allow_blank: true bypasses this test or the validation generally. A password of any number of spaces is accepted which is against the validation in the model. How is that possible?
Any thoughts how to test better (apart from using Rspec!). I bow to your greater knowledge.
TIA.
[EDIT]
The suggested changes in the comments below made my test suite green. This was due to the suite being inadequate. To test the unsuccessful integration, the suggested code tested multiple scenarios in one go, such as:
test "unsuccessful edit with multiple errors" do
log_in_as #user
get edit_user_path(#user)
assert_template 'users/edit'
patch user_path(#user), params: { user:
{ name: "",
email: "foo#invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit'
assert_select 'div.alert', "The form contains 3 errors."
end
The key part here is getting the number of expected errors correct so that assert_select gives the right result. I didn't. The errors should be blank name, invalid email format, password too short, pwd & confirmation don't match. The error for short password isn't showing.
I decided to pull out two more tests to demonstrate the failure of the validations of password length and presence. The point of allow_blank is to allow the password & confirmation fields to have nothing in them when editing the user profile so it isn't compulsory to enter the password every time the user profile is edited. These tests are:
test "unsuccessful edit with short password" do
log_in_as #user
get edit_user_path(#user)
assert_template 'users/edit'
patch user_path(#user), params: { user:
{ name: #user.name,
email: "foo#valid.com",
password: "foo",
password_confirmation: "foo" } }
assert_select 'div.alert', "The form contains 1 error."
end
test "unsuccessful edit with blank (spaces) password" do
log_in_as #user
get edit_user_path(#user)
assert_template 'users/edit'
patch user_path(#user), params: { user:
{ name: #user.name,
email: "foo#valid.com",
password: " ",
password_confirmation: " " } }
assert_select 'div.alert', "The form contains 1 error."
end
If the password is changed, then the validation rules should apply, i.e. the password should not be blank and must have a minimum length. That's not what's happening here either in the code the Tutorial book suggests or the amended code using on: :create and on: :edit.
I figured this out so am posting here in case others come across a similar problem.
I amended the validations to include the :update action on the User, rather than just :edit. This covered the action of saving to the database and caught the short password update validations but still allowed passwords made of spaces.
A bit of checking the documentation showed me that using allow_blank: true allows nil and strings made up of spaces. The scenario here wants a nil password to be acceptable, but not a blank one. The alternative validation of allow_nil: true is more appropriate to the scenario here.
The updated code from above looks like, in User.rb:
validates :password, presence: true,
length: { minimum: 6 },
allow_nil: true,
on: [:edit, :update]
validates :password, presence: true,
confirmation: true,
length: { minimum: 6 },
on: :create
The extended test suite is now all green.
I have a simple Rails 4 application, and I'm trying to write some tests to check my user signup is working properly.
I have a problem that when I'm running testcases with my hands, everything works ok, but when I write some integration tests and run them it skips my model's validations, so I'm getting database exceptions like
Minitest::UnexpectedError: ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_username"
test 'unique username violated' do
params = user_params # it is just a hash with User model attributes
User.new(params).save # saving user first time to violate unique after
params[:email] = 'Test#Email2'
assert_no_difference 'User.count' do
post users_path, user: params
end
assert_template 'users/new'
end
Example of my validation is below:
validates :username, :email, \
uniqueness: {uniqueness: true, message: 'Username&email must be unique'}, \
length: {maximum: 50, message: 'Length must be <= 50'}
It's not only this validation issue, but all the rest are not working in tests too.
If I'll validate my user object with something like, inside of a test, it will be false, as it should be.
User.new(params).valid?
My users#create action is below:
def create
#user = User.new user_params
if #user.save
flash[:success] = 'Welcome to the Sample App!'
redirect_to #user
else
render 'new'
end
end
my users#user_params method:
def user_params
user_group = UserGroup.find_by_name 'administrator'
if user_group.nil?
user_group = UserGroup.new name: 'administrator'
user_group.save
end
{username: 'TestUsername', password: 'TestPassword', \
password_confirmation: 'TestPassword', email: 'Test#Email', \
firstname: 'TestFirstname', lastname: 'TestLastname',user_group: user_group}
end
Rails validations don't really work that way. When you use:
validates :username, :email, uniqueness: true
You are adding a similar but separate validations for :username and :email. Consider this case:
user = User.new
user.valid?
assert(user.errors.has_key?(:username)) # pass
assert(user.errors.has_key?(:email)) # pass
When you print out the errors with user.errors.full_messages() it will contain both Email has already been taken and Username has already been taken.
But if you override the message like so:
validates :username, :email, uniqueness: { message: 'Username&email must be unique' }
You will get Username&email must be unique twice instead, which is not so great.
If for some reason you really need to have only one error you would add a custom validation method:
validate :uniqueness_of_username_and_email
def uniqueness_of_username_and_email
if User.where("username = ? OR email = ?", username, email).any?
errors.add(:email_username, 'Username&email must be unique')
end
end
So I have been racking my brain at this and maybe some of you might have a better idea on how to do proper unit test for this User model. My basic unit test looks like this.
test "should not save without name" do
user = User.new
user.email = "test#test.com"
user.password = "letmein"
assert !user.save
end
This test passes with this model.
class User < ActiveRecord::Base
include Clearance::User
validates :name, presence: true
has_and_belongs_to_many :contests
end
Is there a better way to do this in Clearance? It is nice the gem lets you create users like this on the fly by arbitrarily assigning email and password but I'm thinking maybe I shouldn't have to do this.
user = User.new(:email => "test#test.com", :password => "letmein")
and then,
assert !user.valid?
or
user.should_not be_valid
or
expect { user.save }.to change(User, :count).by(0)
Im trying to test my successfully creates a new user after login (using authlogic). Ive added a couple of new fields to the user so just want to make sure that the user is saved properly.
The problem is despite creating a valid user factory, whenever i try to grab its attributes to post to the create method, password and password confirmation are being ommitted. I presuem this is a security method that authlogic performs in the background. This results in validations failing and the test failing.
Im wondering how do i get round this problem? I could just type the attributes out by hand but that doesnt seem very dry.
context "on POST to :create" do
context "on posting a valid user" do
setup do
#user = Factory.build(:user)
post :create, :user => #user.attributes
end
should "be valid" do
assert #user.valid?
end
should_redirect_to("users sentences index page") { sentences_path() }
should "add user to the db" do
assert User.find_by_username(#user.username)
end
end
##User factory
Factory.define :user do |f|
f.username {Factory.next(:username) }
f.email { Factory.next(:email)}
f.password_confirmation "password"
f.password "password"
f.native_language {|nl| nl.association(:language)}
f.second_language {|nl| nl.association(:language)}
end
You definitely can't read the password and password_confirmation from the User object. You will need to merge in the :password and :password_confirmation to the #user.attributes hash. If you store that hash somewhere common with the rest of your factory definitions, it is not super dry, but it is better than hardcoding it into your test.