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
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)
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
To my User model I have added a new variable new_email. To this end I added:
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :new_email, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
In migration file:
t.string :new_email
I have not added anything in my seeds file regarding this new variable. On seeding I get the error: ActiveRecord::RecordInvalid: Validation failed: New email is invalid. If I remove the validates line from the model file it seeds succesfully. I have tried resetting my db: rake db:drop, rake db:create, rake db:migrate and rake db:seed, but with the same result. What could be causing this error?
The controller methods using params:
def create
#user = User.new(usernew_params)
if #user.save
#user.send_activation_email
flash[:success] = "A confirmation email has been sent to you"
redirect_to root_url
else
render 'new'
end
end
def update
#user = User.friendly.find(params[:id])
if #user.update_attributes(userupdate_params)
flash[:success] = "Profile updated"
redirect_to #user
else
render 'edit'
end
end
private
def usernew_params
params.require(:user).permit(:email,
:username,
:password,
:password_confirmation)
end
def userupdate_params
params.require(:user).permit(:email,
:email_new,
:avatar,
:organization,
:activated,
:password,
:password_confirmation)
end
And my seeds file (left out other models without a relationship to this model):
99.times do |n|
username = "fakename#{n+1}"
email = "example-#{n+1}#railstutorial.org"
password = "password"
organization = Faker::Company.name
User.create!(username: username,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now,
organization: organization)
end
The issue is that you are validating new_email on every request and not allowing it to be blank. Since it is not set in your seed file and you are validating based on a Regexp this will fail every time.
Based on your use case described I would recommend this
validates :new_email, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
allow_blank: true,
on: :update
This will do 2 things:
It will allow new_email to be blank "" or omitted completely nil without failing.
Also since you are not even accepting new_email on create through the controller this validation will only run on update.
While validations that allow_blank are fairly lightweight I still try to advise against running code when it is not needed that is why I added the on parameter.
After following the Hartl Tutorial I'm trying to change the authentication to use the devise gem. My sample application site seems to be working again but some of the specs still fail because some of the routes and user controller actions have changed. So I'm in the process of fixing those and stuck on one that checks to make sure the user can't give themselves admin access.
describe "update user with forbidden attributes", type: request do
FactoryGirl.create(:user)
let(:params) do
{ "user[name]" => "new name",
"user[email]" => user.email,
"user[current_password]" => user.password,
"admin" => true }
end
before do
post user_session_path, 'user[email]' => user.email, 'user[password]' => user.password
patch user_registration_path(user), params
user.reload
end
its(:name) { should eql "new name" } # passes, and should.
its(:admin?) { should be false } # can't get to fail.
specify { expect(response).to be_success } # fails, gets response 406.
end
This test passes, but it passes because I can't get it to fail. I'm trying to do the usual Red-Green-Refactor and I can't make it go red, even if I add admin to the list of devise acceptable parameters. I want to make sure that this would change admin if the permissions were screwed up.
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: devise_controller?
after_action :print_permitted_parameters, if: devise_controller?
def configure_permitted_parameters
...
devise_parameter_sanitizer.for(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :current_password, :admin)
end
end
def print_configured_parameters
puts "sign_up: " + devise_parameter_sanitizer.for(:sign_up).join(' ')
#prints "sign_up: email password password_confirmation"
puts "sign_in: " + devise_parameter_sanitizer.for(:sign_in).join(' ')
#prints "sign_in: email password remember_me"
puts "account_update: " + devise_parameter_sanitizer.for(:account_update).join(' ')
#prints "account_update: email password password_confirmation current_password"
end
end
The strange thing is that user's name and email do update, so something is working. But the response I get is always 406 for "Not Acceptable". So my question is why can I not get the admin tests to fail? And are the 406 errors related?
printing the permitted parameters suggests the parameters aren't being configured for any actions, it's just the default list. And I can sign_in with an existing user but if I just click "sign_in" with no fields it complains of an umpermitted parameter: "remember_me" despite that being on the list. Similarly if I try to sign_up a new user, which used to work, it complains that password_confirmation is unpermitted.
Thanks for your help, I appreciate it.
I set up a User AR model which has conditional validation which is pretty much identical to the Railscast episode on conditional validation. So basically my User model looks like this:
class User < ActiveRecord::Base
attr_accessor :password, :updating_password
validates :password, :presence => true,
:confirmation => true,
:length => { :within => 6..40 },
:if => :should_validate_password?
def should_validate_password?
updating_password || new_record?
end
end
Now in my action where the User can change their password I have the following two lines:
#user.updating_password = true
if #user.update_attributes(params[:user]) ...
so that I flag the validations to be run on the password. In development mode this works great - if the user tries to put in a password that is too short or too long the model does not pass validation. My problem is that for the life of me I can not get my tests for this to pass. Here is my spec:
require 'spec_helper'
describe PasswordsController do
render_views
before(:each) do
#user = Factory(:user)
end
describe "PUT 'update'" do
describe "validations" do
before(:each) do
test_sign_in(#user)
end
it "should reject short passwords" do
short = "short"
old_password = #user.password
#attr2 = { :password => short, :password_confirmation => short }
put :update, :user_id => #user, :old_password => #user.password, :user => #attr2
#user.password.should == old_password
end
it "should reject long passwords" do
long = "a" * 41
old_password = #user.password
#attr2 = { :password => long, :password_confirmation => long }
put :update, :user_id => #user, :old_password => #user.password, :user => #attr2
#user.password.should == old_password
end
end
end
end
When I run these tests I always get the error:
1) PasswordsController PUT 'update' validations should reject short passwords
Failure/Error: #user.password.should == old_password2
expected: "foobar"
got: "short" (using ==)
and of course the error for the password being too long. But should'nt the password be validated as a result of me setting #user.updating_password = true before any save attempts in the controller?
I think the problem isn't the code but what you expect it to do. When you call update_attributes and pass in a bad value, the value is saved into the model object even though the validation fails; the bad value has not been pushed to the database.
I think this makes sense because when the validation fails normally you would show the form again with the error messages and the inputs populated with the bad values that were passed in. In a Rails app, those values usually come from the model object in question. If the bad values weren't saved to the model they would be lost and your form would indicate that the old 'good' values had failed validation.
Instead of performing this check:
#user.password.should == old_password
Maybe try:
#user.errors[:password].should_not == nil
or some other test that makes sense.