Testing Rails model uniqueness with duplicate entry - ruby-on-rails

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)

Related

Password validation fails in two opposed scenarios

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.

Rails model validates and tests

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

Validate uniqueness of a value for two columns Rails

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

Rspec be_valid fails on a valid test

I'm learning TDD with Rails 4 and rspec. I've made some test cases for my user model to check the password lengths. I have two tests so far that checks whether a user input a password that was too short and one where the password is between 6 - 10 characters.
So far, the "password is too short" test passes:
it "validation says password too short if password is less than 6 characters" do
short_password = User.create(email: "tester#gmail.com", password: "12345")
expect(short_password).not_to be_valid
end
However, on the test where I do have a valid password, it fails:
it "validation allows passwords larger than 6 and less than 10" do
good_password = User.create(email: "tester2#gmail.com", password: "blahblah")
expect(good_password).to be_valid
end
And I get this error:
Failure/Error: expect(good_password).to be_valid
expected #<User id: 1, email: "tester2#gmail.com",
created_at: "2014-06-21 02:43:42", updated_at: "2014-06-21 02:43:42",
password_digest: nil, password: nil, password_hash: "$2a$10$7u0xdDEcc6KJcAi32LBW7uzV9n7xYbfOhZWdcOnU5Cdm...",
password_salt: "$2a$10$7u0xdDEcc6KJcAi32LBW7u"> to be valid,
but got errors: Password can't be blank, Password is too short (minimum is 6 characters)
# ./spec/models/user_spec.rb:12:in `block (3 levels) in <top (required)>'
Here's my model code:
class User < ActiveRecord::Base
has_many :pets, dependent: :destroy
accepts_nested_attributes_for :pets, :allow_destroy => true
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
validates :password, presence: true, :length => 6..10, :confirmation => true
#callbacks
before_save :encrypt_password
after_save :clear_password
#method to authenticate the user and password
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
#method to encrypt password
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
#clears password
def clear_password
self.password = nil
end
end
I'm confused on why the password is nil when I create the test object.
You have a password presence requirement on your model, but then you have an after_save hook that nilifies the password and puts the record into an invalid state. The first test passes because your records are always being put into an invalid state by the after_save hook. You need to rethink how you're handling password storage; once you resolve that, here are some code samples to help give you some ways to test this:
# Set up a :user factory in spec/factories.rb; it should look something like:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "tester+#{n}#gmail.com" }
password { SecureRandom.hex(6) }
end
end
# In your spec:
let(:user) { create :user, password: password }
context 'password' do
context 'length < 6' do
let(:password) { '12345' }
it { expect(user).not_to be_valid }
it { user.errors.message[:password]).to include('something') }
end
context 'length >= 6' do
context 'length < 10' do
let(:password) { 'blahblah' }
it { expect(user).to be_valid }
end
context 'length >= 10' do
let(:password) { 'blahblahblah' }
it { expect(user).not_to be_valid }
end
end
end
You can also use shoulda matchers:
it { should_not allow_value('12345').for(:password) }
it { should allow_value('12345blah').for(:password) }
The most likely problem is the password field is not mass assignable. That is why password is nil in the output message.
Try this instead:
it "validation allows passwords larger than 6 and less than 10" do
good_password = User.create(email: "tester2#gmail.com")
good_password.password = "blahblah"
expect(good_password).to be_valid
end
Note that your first test is passing accidentally - it has the same problem as the second test (password isn't being assigned). This means you aren't actually testing that the password is rejected when less than 6 characters atm.
See this article on mass assignment for more details.
EDIT: Leo Correa's comment may suggest this may not be the case for you. Posting your model code would help...

Devise/Rspec - Tested a user creation with (maybe) missing attributes (got true..)

I am testing Devise with Rspec using Micheal Hartl source code (railstutorial)
Whereas the confirmable module is enabled, I don't understand why this test pass:
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before(:each) do
#attr = { :username => "ExampleUser",
:email => "user#example.com",
:password => 'test1234',
}
end
it "should create a new instance given valid attributes" do
User.create!(#attr)
end
end
Basically, I want to be sure of this code does, it tests the creation on the user, not this validation (cause the user has not confirmed yet and the test returns true) ? This is right?
Moreover, I didn't provide attribute for password confirmation, and the user is still created!
Is this mean that in the :validatable module there is not (?):
validates :password, :confirmation => true
Thanks to get you view on this!
one problem is the trailing comma at the end of your each block. second, you are not asserting anything in your test to pass or fail the test, though you are probably erroring out at this point, which is why you are saying it didnt pass.
you can try assigning the user object to a variable:
it "should create a new instance given valid attributes" do
#user = User.new(#attr)
#user.should be_valid #=> new will let you know if its valid or not
#user.save.should be_true #=> another possible assertion to pass/fail the test
# debug message to give you back why it failed
puts #user.errors.full_messages.to_sentence
end

Resources