简体   繁体   中英

Rails RSpec test for patch update (via web) of associated model

I'm doing the Michael Hartl Rails tutorial (using Rails 4 and RSpec-Rails 3.3.3) and implementing users with admin privileges, which it achieves by adding an admin boolean attribute to the User model. I've instead decided to use a spearate Admin model (User has_one Admin; Admin belongs_to User), which simply stores the user_id; if a user's id exists in the table then it is an admin (I feel this will be more efficient long-term given the anticipated ratio of admins to non-admins).

A user controller test is suggested to ensure that the permitted params do not allow the admin attribute to be edited via the web like so (user is created in the database using Factory Girl):

patch :update, id: user, user: { password: user[:password], password_confirmation: user[:password_confirmation], admin: '1' }

To test my version, I've attempted the following RSpec test:

context 'attempt to assign non-admin user as admin via update request via web' do
  it 'will not update admin status' do
    session[:user_id] = user.id # user is logged in as required to make patch request
    expect { patch :update, id: user, user: { name: user.name, email: user.email, admin: { user_id: user.id } } }.to change { Admin.count }.by 0
  end
end

This tests passes, but not by merit of the permitted params being correctly restricted. To check that the inverse works, I want to test that allowing the admin params in the User model will actually make the change to the Admin table.

I have updated user_controller:

def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirmation, admin: :user_id)
end

And users_controller_spec (checks Admin count changes by 1):

context 'attempt to assign non-admin user as admin via update web request' do
  it 'will not update admin status' do
    session[:user_id] = user.id
    expect { patch :update, id: user, user: { name: user.name, email: user.email, admin: { user_id: user.id } } }.to change { Admin.count }.by 1
  end
end

Which gives this error (docs say it is raised when an object assigned to an association has an incorrect type):

Failure/Error: expect { patch :update, id: user, user: { name: user.name, email: user.email, admin: { user_id: user.id } } }.to change { Admin.count }.by 1
     ActiveRecord::AssociationTypeMismatch:
       Admin(#70320649341140) expected, got ActionController::Parameters(#70320662053980)

I have a feeling I may have this entirely the wrong way round: that the Admin model should only be updated directly rather than through the User model, and perhaps by separating Admin out into a separate model like this that such a malicious patch request to give a user admin status may not even be possible (there is no admin_controller or admin routes as I anticipate such assignation will be made at database level; only a simple Admin model which states its belongs_to association with User).

I'd really appreciate some advice on whether there is a test I should be writing given my circumstances.

Thanks in advance.

btw, does Michael Hartl not have his own support forum, and/or a specific tag on SO?

so I'm getting the error on the branch:

1) UsersController attempt to assign non-admin user as admin via update web request will update admin status
     Failure/Error: expect { patch :update, id: user, user: { name: user.name, email: user.email, admin: { user_id: user.id } } }.to change { Admin.count }.by 1
     ActiveRecord::AssociationTypeMismatch:
       Admin(#70235772428120) expected, got ActionController::Parameters(#70235708075860)

one general tip is just don't write controller specs ... prefer only features and unit tests

here's how I'm dissecting the problem

[tansaku@Samuels-MBP-2:~/Documents/Github/MakersAcademy/Students/April2015/AndyGout/theatrebase ((ac406cd...))]$ 
→ rspec ./spec/controllers/users_controller_spec.rb:89
Run options: include {:locations=>{"./spec/controllers/users_controller_spec.rb"=>[89]}}

UsersController
  attempt to assign non-admin user as admin via update web request

[29, 38] in /Users/tansaku/Documents/Github/MakersAcademy/Students/April2015/AndyGout/theatrebase/app/controllers/users_controller.rb
   29:     @page_title = @user.name
   30:   end
   31: 
   32:   def update
   33:     require 'byebug' ; byebug
=> 34:     if @user.update_attributes(user_params)
   35:       flash[:success] = "Profile updated successfully: #{@user.name}"
   36:       redirect_to @user
   37:     else
   38:       @page_title = User.find(params[:id]).name
(byebug) @user
#<User:0x007fede0b81b60>
(byebug) user_params
{"name"=>"Andy Gout", "email"=>"andygout@example.com", "admin"=>{"user_id"=>"1"}}

I think the problem here seems like you aren't set up to create the new admin object via a simple params hash

I see two ways round this

  1. manipulate the params to insert a new admin object
  2. use accepts_nested_attributes_for http://guides.rubyonrails.org/form_helpers.html#building-complex-forms

Thanks @SamJoseph!

I'll also check for Hartl support (although am conscious I'm deviating from his guide...).

It was the second suggestion that helped me to a solution, using the following changes:-

user (User model):

accepts_nested_attributes_for :admin

users_controller:

def user_params
  params.require(:user).permit( :name,
                                :email,
                                :password,
                                :password_confirmation,
                                admin_attributes: :user_id
                              )
end

users_controller_spec:

  context 'attempt to assign non-admin user as admin via update web request' do
    it 'will update admin status' do
      session[:user_id] = user.id
      expect { patch :update, id: user, user: { name: user.name, email: user.email, admin_attributes: { user_id: user.id } } }.to change { Admin.count }.by 1
    end
  end

This passes the test, so I can now work confidently from that to test the inverse as I require.

It's doubly useful as accepts_nested_attributes_for is something I'll be needing again shortly.

I was slightly unsure by what you meant in the first suggestion: manipulate the params to insert a new admin object - is this somehow adding a command within the params to directly create a new Admin entry in the database (or something to that effect)? I will investigate further.

Can I also check re. not writing controller specs: given this seems an important test, would it be better to write a feature spec that makes this direct patch request? What would you recommend?

Thanks again!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM