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.
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)
Description of the Exercise: Railstutorial Exercise 10.4.1.2
The exercise: 'FILL_IN' has to be replaced with the proper code, so that the test is working
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: FILL_IN,
password_confirmation: FILL_IN,
admin: FILL_IN} }
assert_not #other_user.FILL_IN.admin?
end
My solution:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'password',
password_confirmation: 'password',
admin: true } }
assert_not #other_user.reload.admin?
end
My Question(s):
If you have set the ':admin' attribute to the list of permitted parameters in user_params (in the UsersController class) the test turns 'Red' as it is supposed to. What I don't understand though, is that you can set random passwords and the test is still working properly, like so:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'foobar',
password_confirmation: 'foobar',
admin: true } }
assert_not #other_user.reload.admin?
end
Shouldn't the only valid option be 'password' (and not 'foobar' or even '' (i.e. blank)), since the instance variable #other_user contains the values of the User 'archer' from the fixtures file 'users.yml', who has 'password' as a password_digest? Wouldn't it result in a mismatch between the two attributes password_digest(='password') and password(='foobar')? Or is the 'password_digest' attribute from 'archer' somehow updated as well? If so, how does it work?
And why does the test turn 'Green', if you type in an invalid password, like:
test "should not allow the admin attribute to be edited via the web" do
log_in_as(#other_user)
assert_not #other_user.admin?
patch user_path(#other_user), params: {
user: { password: 'foo',
password_confirmation: 'bar',
admin: true } }
assert_not #other_user.reload.admin?
end
Could it be that the 'patch' request aborts due to the incorrect password input and thus also fails to update the admin status? Is that the reason why the test is 'Green' (since admin is still 'nil')?
I already looked up the reference implementation of the sample application by Michael Hartl (https://bitbucket.org/railstutorial/sample_app_4th_ed), but unfortunately he does not provide any code relating to the exercises.
Thanks for help!
In Chapter 10.1, for the update tests, we allowed empty password for tests on the User Controller :
class User < ApplicationRecord
.
.
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
.
.
end
As mentionned in the same chapter, the password is secured with the has_secure_password helper for the online use.
Hope I helped
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
Using Ruby on Rails 4.2.0.rc2 I added an 'Accept terms of service' checkbox to user registration
In the user model I added
attr_accessor :terms_of_service
validates_acceptance_of :terms_of_service, acceptance: true
In the view
<%= f.check_box :terms_of_service %>
and finally in the controller I added it to the list of parameters
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :terms_of_service)
end
This works as expected but since I made a change to the implementation I expected the related tests to be in the red. However, this test passes and I don't understand why:
assert_difference 'User.count', 1 do
post users_path, user: { name: "Example User",
email: "user#example.com",
password: "password",
password_confirmation: "password" }
end
I can re-write my tests like so
test "accept terms of service" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "Example User",
email: "user#example.com",
password: "password",
password_confirmation: "password",
terms_of_service: "0" }
end
assert_difference 'User.count', 1 do
post users_path, user: { name: "Example User",
email: "user#example.com",
password: "password",
password_confirmation: "password",
terms_of_service: "1" }
end
end
but I am curious as to why the original test fails to fail. What I've taken away from this is that validates_acceptance_of passes for nil.
Is this the intended behaviour?
In a nutshell, yes, nil is allowed. I've had the same issue before.
active_model/validations/acceptance.rb
module ActiveModel
module Validations
class AcceptanceValidator < EachValidator # :nodoc:
def initialize(options)
super({ allow_nil: true, accept: "1" }.merge!(options))
setup!(options[:class])
end
# ...
end
# ...
end
# ...
end
In the initializer, it merges allow_nil with the options, so yes, nil (or the lack of a value, I should say) is allowed for that. They mention it in the Rails Guide for acceptance, but I missed it.
This bit me a few times in my tests also - I kept getting passing validations when I was certain they should not pass. Now we know why!
For reasons outside of my control, I can't use RSpec for testing in my current project. I'm trying to test Devise Reset Password, and I can't seem to come up with something that works.
Here's what I have so far:
require 'test_helper'
class ResetPasswordTest < ActionDispatch::IntegrationTest
setup do
#user = users(:example)
end
test "reset user's password" do
old_password = #user.encrypted_password
puts #user.inspect
# This assertion works
assert_difference('ActionMailer::Base.deliveries.count', 1) do
post user_password_path, user: {email: #user.email}
end
# puts #user.reset_password_token => nil
# Not sure why this doesn't assign a reset password token to #user
patch "/users/password", user: {
reset_password_token: #user.reset_password_token,
password: "new-password",
password_confirmation: "new-password",
}
# I get a success here, but I'm not sure why, since reset password token is nil.
assert_response :success
# This assertion doesn't work.
assert_not_equal(#user.encrypted_password, old_password)
end
end
I've added some comments above to where things don't seem to be working. Does anyone have an insight, or an idea about how better to test this?
Devise tests the PasswordsController internally. I wanted to test my PasswordsController with extra functionality and went to Devise's controller tests for help in building tests for Devise's default functionality, then adding assertions for my new functionality into the test case.
As noted in other answers, you must obtain the reset_password_token. Instead of parsing a delivered email as in another solution here, you could obtain the token in the same way as Devise:
setup do
request.env["devise.mapping"] = Devise.mappings[:user]
#user = users(:user_that_is_defined_in_fixture)
#reset_password_token = #user.send_reset_password_instructions
end
Check out Devise's solution for the PasswordControllerTest:
https://github.com/plataformatec/devise/blob/master/test/controllers/passwords_controller_test.rb.
I figured out that you need to reload the user upon sending the password reset email and when putting the /user/password route. You also need to get the password token from the email as it is different from the one stored in the database.
require 'test_helper'
class ResetPasswordTest < ActionDispatch::IntegrationTest
setup do
#user = users(:example)
end
test "reset user's password" do
# store old encrypted password
old_password = #user.encrypted_password
# check to ensure mailer sends reset password email
assert_difference('ActionMailer::Base.deliveries.count', 1) do
post user_password_path, user: {email: #user.email}
assert_redirected_to new_user_session_path
end
# Get the email, and get the reset password token from it
message = ActionMailer::Base.deliveries[0].to_s
rpt_index = message.index("reset_password_token")+"reset_password_token".length+1
reset_password_token = message[rpt_index...message.index("\"", rpt_index)]
# reload the user and ensure user.reset_password_token is present
# NOTE: user.reset_password_token and the token pulled from the email
# are DIFFERENT
#user.reload
assert_not_nil #user.reset_password_token
# Ensure that a bad token won't reset the password
put "/users/password", user: {
reset_password_token: "bad reset token",
password: "new-password",
password_confirmation: "new-password",
}
assert_match "error", response.body
assert_equal #user.encrypted_password, old_password
# Valid password update
put "/users/password", user: {
reset_password_token: reset_password_token,
password: "new-password",
password_confirmation: "new-password",
}
# After password update, signed in and redirected to root path
assert_redirected_to root_path
# Reload user and ensure that the password is updated.
#user.reload
assert_not_equal(#user.encrypted_password, old_password)
end
end
require 'rails_helper'
feature 'User' do
let(:user) { create(:user) }
let(:new_password) { 'Passw0rd!' }
it 'reset password' do
visit '/'
click_link 'Forgot password?'
fill_in 'E-mail', with: user.email
expect do
click_button 'Send me reset password instructions'
end.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(unread_emails_for(user.email)).to be_present
open_email(user.email, with_subject: 'Reset password instructions')
click_first_link_in_email
fill_in 'New password', with: new_password
fill_in 'Confirm new password', with: new_password
click_button 'Change password'
expect(page).to have_notice 'Your password has changed'
click_link 'Logout'
fill_in 'E-mail', with: user.email
fill_in 'Password', with: new_password
click_button 'Sign in'
expect(page).to have_notice 'Wellcome!'
end
end
This code require email_spec and FactoryGirl gems.