0
votes

FEATURE

A user has a profile and should be able to update it.


PROBLEM

I update the profile, for example change the name to "Homer Simpson", but all the assertions fail as the database record does not seem to update.

I can't seem to get updated attributes:

 Failure/Error: expect(subject.current_user.first_name).to eq('Homer')

   expected: "Homer"
        got: "Lew"

   (compared using ==)
 # ./spec/controllers/registrations_controller_spec.rb:67:in `block (3 levels) in <top (required)>'

N.B. I have tried both @user.reload and subject.current_user.reload

Specs still not passing.


CODE

I am using:

  • rails (4.0.0)
  • devise (3.0.3)
  • rspec-rails (2.14.0)
  • capybara (2.1.0)
  • factory_girl (4.2.0)
  • database_cleaner (1.1.1)

I have already checked:

registrations_controller_spec.rb

describe "User Profiles" do
  login_user

  it "Update - changes the user's attributes" do
    put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
    @user.reload
    expect(@user.first_name).to eq('Homer') # FAILS
  end
end

I have tried swapping @user for subject.current_user like in this Stackoverflow thread: "Devise Rspec registration controller test failing on update as if it was trying to confirm email address"

  put :update, id: subject.current_user, user: attributes_for(:user, first_name: 'Homer')
  subject.current_user.reload
  expect(subject.current_user.first_name).to eq('Homer') # Still FAILS

But it still fails.

Is the problem in the controller? I find the user by current_user.id instead of via params[:id].

registrations_controller.rb

def update
  @user = User.find(current_user.id)
  email_changed = @user.email != params[:user][:email]
  password_changed = !params[:user][:password].blank?

  if email_changed or password_changed
    successfully_updated = @user.update_with_password(user_params)
  else
    successfully_updated = @user.update_without_password(user_params)
  end

  if successfully_updated
    sign_in @user, bypass: true # Sign in the user bypassing validation in case his password changed
    redirect_to user_profile_path, notice: 'Profile was successfully updated.'
  else
    render "edit"
  end
end

controller_macros.rb - defines login_user helper

module ControllerMacros
  def login_user    
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:user]
      @user = FactoryGirl.create(:user)
      @user.confirm!
      sign_in @user
    end
  end
end

My integration specs pass fine. What am I missing here in the controllers?

3

3 Answers

2
votes

My answer can solve your problem but not a direct fix of the bugs in your code. To do that I need to write more tests and hands-on debugging, I'm not so experienced to figuring it out by reading only :)

I do not suggest you to override Devise's RegistrationsController like that in question. Comparing with original code, your code lacks of two things at least:

  1. No copy of current_user object. In real app the current_user will be logged out by submitting the form which is not nice.

  2. Lack of sanitize of parameters

And the remaining bugs.

My suggestion is to use Devise's method directly because there is nothing special in your code and no need to override full code.

class RegistrationsController < Devise::RegistrationsController
  def update
  end
  # Or even without this method.
end

That's all.

For no requiring of password

def update
  params.merge!(password: current_user.password) if params[:password].blank?
  super
end

For tests, just write some casual integration tests. Devise has full covered functional tests so no need to repeat.

0
votes

try assigns

it "Update - changes the user's attributes" do
  put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
  homer = assigns(:user)
  @user.reload
  expect(homer.first_name).to eq('Homer')
end

update: based on Billy Chan's comment, this should correctly test that the name is being updated

it "Update - changes the user's attributes" do
  put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
  homer = assigns(:user)
  @user.reload
  #expect(homer.first_name).to eq('Homer') Peter and Billy are right, this only tests
  # that the attribute was actually assigned, not that the update was successful
  expect(@user.first_name).to eq(homer.first_name)
  #however this test that the users updated `first_name` matches the attribute 
  #in the test 
end

Note:

I'm basing this answer on the Michael Hartl tutorial which I went through a few months ago - he uses this method, and I believe he explains why - although I don't have the screen casts in front of me at the moment. I'll look it up later.

Video:

Here's the video - it's very low quality because i just used quicktime's screen record - and there's some a brutal feedback loop in the beginning so mute your computer for the first few seconds.

0
votes

Answer

Thanks everyone for their suggestions, which helped me wrap my head around my code and find the problem.

CAUSE OF FAIL: Default factory included params included email and password, so controller test kept trying to change the user's password.

Specifically I changed this line of code in registrations_controller_spec.rb

put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')

to:

patch :update, id: @user, user: attributes_for(:user_params, first_name: 'Homer', last_name: 'Simpson')

Then I had to update my factory, so I can use :user_params instead for updates:

FactoryGirl.define do

  factory :user do
    first_name          { Faker::Name.first_name }
    last_name           { Faker::Name.last_name }
    sequence(:username) { |n| "user-#{n}" }
    email               { Faker::Internet.email }
    password            { Faker::Lorem.characters(8) }
  end

  factory :user_params, class: :user do
    first_name     { Faker::Name.first_name }
    last_name      { Faker::Name.last_name }

    factory :user_params_with_email, class: :user do
      email        { Faker::Internet.email }
    end

    factory :user_params_with_password, class: :user do
      password    { Faker::Lorem.characters(8) }
    end
  end

end

Thanks for everyone who made suggestions. It helped me unravel my code and @billy-chan was right in pointing out issues, which I fixed.

  • Params weren't sanitized (am in process of finishing rails4 upgrade)
  • Other misc. bugs

Lesson learned

Compare params going in and out of controllers.

My integration tests were passing because I wasn't trying to change the email or password.