Make Rails Class Attribute Inaccessible - Rails Tutorial Chapter 9, Exercise 1 - ruby-on-rails

I'm working through Michael Hartl's Rails Tutorial. I've come to Chapter 9, Exercise 1. It asks you to add a test to verify that the admin attribute of the User class is not accessible. Here's the User class with irrelevant portions commented out:
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
attr_protected :admin
# before_save methods
# validations
# private methods
end
And here's the test I'm using to validate that the admin attribute is not accessible.
describe User do
before do
#user = User.new(
name: "Example User",
email: "user#example.com",
password: "foobar123",
password_confirmation: "foobar123")
end
subject { #user }
describe "accessible attributes" do
it "should not allow access to admin" do
expect do
#user.admin = true
end.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
end
end
end
The test fails. It says no errors were raised, in spite of the fact that the admin attribute is protected. How can I get the test to pass?

From the Ruby documentation:
Mass assignment security provides an interface for protecting attributes from end-user assignment.
http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity/ClassMethods.html
Try this code instead
describe "accesible attributes" do
it "should not allow access to admin" do
expect do
User.new(admin: true)
end.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
end
end

As Rails docs claim about attr_protected
Attributes named in this macro are protected from mass-assignment, such as new(attributes), update_attributes(attributes), or attributes=(attributes).
So you can change field manually. 'attr_protected' is only about mass-assignment.

This only works for mass assignments like setting the field from a form submit. Try something like this:
#user.update_attrtibutes(:admin => true)
#user.admin.should be_false

#agaved. This answer may come late and you may already have the answer but I wanted to answer your question, it may help somebody else.
The best way to understand how update_attributes differs from direct assignment
#user.admin = true is to try and do it in your console. If you are following Hartl's tutorial, try the following:
#user = User.first
#user.admin?
=> true
#user.admin = false
=> false
Direct assignment manages to change the value of the attribute admin for user from true to false without raising a Mass Assignment Error. This is because Mass Assignment Errors are raised when you call update_attributes or create a new user User.new using an attribute that is not accessible. In other words, Rails raises mass assignment errors when a user tries to update (attribute_update) or create User.new(admin: true) a new user with attributes that are not accessible to her. In the above case, direct assignment is not using the create or update methods of the user controller.
They are very similar pieces of code since you can use direct assignment to force a change in the admin attribute in the above case using #user.save!(validate: false) directly in IRB but as I said above this does not use the create or update method of your user controller and, hence, it will not throw the error.
I hope that helps, this helped me.

[Spoiler alert: If you are trying to solve the exercises in Hartl's book on your own, I'm pretty sure I'm about to give the answer away. Even though the answer that has been accepted is interesting information, I don't believe it was what Hartl had in mind as that would require knowledge the book has not covered and also does not relate it specifically to updates via web action or use the test he provides.]
I think you might be thinking this exercise is a lot harder than it actually is, if I got it right. First of all, you have misunderstood the hint:
Hint: Your first step should be to add admin to the list of permitted parameters in user_params.
It does not say to change its attr declaration in the class. It says to modify the helper function user_params. So I added it to the list in users_controller.rb:
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation, :admin)
end
Next, I copied the code in Listing 9.48 to the indicated place in spec/requests/user_pages_spec.rb:
require 'spec_helper'
describe "User pages" do
.
.
.
describe "edit" do
.
.
.
describe "forbidden attributes" do
let(:params) do
{ user: { admin: true, password: user.password,
password_confirmation: user.password } }
end
before do
sign_in user, no_capybara: true
patch user_path(user), params
end
specify { expect(user.reload).not_to be_admin }
end
end
end
The test then fails, showing that it is possible to pass in an admin parameter and thus change a normal user to an admin, which is not what you would want to allow:
$ rspec spec
.....................[edited out dots].................................F
Failures:
1) User pages edit forbidden attributes
Failure/Error: specify { expect(user.reload).not_to be_admin }
expected admin? to return false, got true
# ./spec/requests/user_pages_spec.rb:180:in `block (4 levels) in <top (required)>'
Finished in 4.15 seconds
91 examples, 1 failure
Failed examples:
rspec ./spec/requests/user_pages_spec.rb:180 # User pages edit forbidden attributes
Then, to make it impossible to pass in an admin value via a web action, I simply removed :admin from the list of acceptable user_params, undoing the first change:
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
Now the attempt to patch the user with a new admin value fails... and the test for it succeeds, verifying "that the admin attribute isn’t editable through the web."
$ rspec spec
...........................................................................................
Finished in 4.2 seconds
91 examples, 0 failures

Following the hint, I first added :admin to attr_accessible in app/models/user.rb to start with a red.
I then added the test:
describe "admin attribute" do
it "should not be accessible" do
expect do
#user.update_attributes(:admin => true)
end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
end
end
to the spec and got a red.
Removing :admin from user.rb I get a green. So far so good.
What puzzles me is why I should use the sintax:
#user.update_attributes(:admin => true)
instead of #user.admin = true (I checked and in this case it doesn't work).

Related

What is the proper way to test that a controller appropriately handles a uniqueness validation?

Summary
I am building a Rails app which includes a user registration process. A username and password are necessary to create a user object in the database; the username must be unique. I am looking for the right way to test that the uniqueness validation prompts a particular action of a controller method, namely UsersController#create.
Context
The user model includes the relevant validation:
# app/models/user.rb
#
# username :string not null
# ...
class User < ApplicationRecord
validates :username, presence: true
# ... more validations, class methods, and instance methods
end
Moreover, the spec file for the User model tests this validation using shoulda-matchers:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
it { should validate_uniqueness_of(:username)}
# ... more model tests
end
The method UsersController#create is defined as follows:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
render :show
else
flash[:errors] = #user.errors.full_messages
redirect_to new_user_url
end
end
# ... more controller methods
end
Since the User spec for username uniqueness passes, I know that a POST request which contains a username already in the database will cause UsersController#create to enter the else portion of the conditional, and I want a test to verify this situation.
Currently, I test how UsersController#create handles the uniqueness validation on username in the following manner:
# spec/controllers/users_controller_spec.rb
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe 'POST #create' do
context "username exists in db" do
before(:all) do
User.create!(username: 'jarmo', password: 'good_password')
end
before(:each) do
post :create, params: { user: { username: 'jarmo', password: 'better_password' }}
end
after(:all) do
User.last.destroy
end
it "redirects to new_user_url" do
expect(response).to redirect_to new_user_url
end
it "sets flash errors" do
should set_flash[:errors]
end
end
# ... more controller tests
end
Issue
My primary concern is the before and after hooks. Without User.last.destroy, this test will fail when run in the future: The new record can't be created, and thus the creation of a second record with the same username doesn't occur.
Question
Should I be testing this particular model validation in the controller spec? If so, are these hooks the right/best way to accomplish this goal?
I'll steer away from an opinion on the 'should I...' part, but there are a couple of aspects worth considering. First, although controller tests have not been formally deprecated, they have generally been discouraged by both the Rails and Rspec teams for a while now. From the RSpec 3.5 release notes:
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
Whether or not the scenario warrants a corresponding request spec is a judgement call, but if you want to unit test the validation at the model level, check out the shoulda matchers gem, which assists with model validation testing).
In terms of your question about hooks, before(:all) hooks run outside a database transaction, so even if you have use_transactional_fixtures set to true in your RSpec configuration, they won't be automatically rolled back. So, a matching after(:all) like you have is the way to go. Alternatives include:
Creating the user inside a before(:each) hook, which does run in a transaction and is rolled back. That's at the potential cost of some test performance.
Use a tool like the Database Cleaner gem, which gives you fine-grained control over the strategies for cleaning your test databases.
If you want to cover the controller together with the user feedback aspect of this I would suggest a feature spec:
RSpec.feature "User creation" do
context "with duplicate emails" do
let!(:user) { User.create!(username: 'jarmo', password: 'good_password') }
it "does not allow duplicate emails" do
visit new_user_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'p4ssw0rd'
fill_in 'Password Confirmation', with: 'p4ssw0rd'
expect do
click_button 'Sign up'
end.to_not change(User, :count)
expect(page).to have_content 'Email has already been taken'
end
end
end
Instead of poking inside the controller this drives the full stack from the user story and tests that the view actually has an output for the validation errors as well - it thus provides value where a controller spec provides very little value.
Use let/let! to setup givens for a particular example as it has the advantage that you can reference them in the example through the helper method it generates. before(:all) should generally be avoided apart from stuff like stubbing out API's. Each example should have its own setup/teardown.
But you also need to deal with the fact that the controller itself is broken. It should read:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
render :new, status: :unprocessable_entity
end
end
end
When a record is invalid you should NOT redirect back. Render the form again as you're displaying the result of performing a POST request. Redirecting back will make for a horrible user experience since all the fields will be blanked out.
When creating a resource is successful you should redirect the user to the newly created resource so that the browser URL actually points to the new resource. If you don't reloading the page will load the index instead.
This also removes the need to stuff the error messages in the session. If you want to give useful feedback through the flash you would do it like so:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
flash.now[:error] = "Signup failed."
render :new, status: :unprocessable_entity
end
end
end
And you can test it with:
expect(page).to have_content "Signup failed."

Rspec not logging me in

I was looking at this answer to see how to test a session controller and wrote something like this:
require 'spec_helper'
describe SessionsController do
context "We should login to the system and create a session" do
let :credentials do
{:user_name => "MyString", :password => "someSimpleP{ass}"}
end
let :user do
FactoryGirl.create(:user, credentials)
end
before :each do
post :create , credentials
end
it "should create a session" do
puts user.inspect
puts session[:user_id]
#session[:user_id].should == user.id
end
end
end
Based on that I created a factory girl user:
FactoryGirl.define do
factory :user, :class => 'User' do
name "sample_user"
email "MyString#gmail.com"
user_name "MyString"
password "someSimpleP{ass}"
end
end
Now it all works - exceot for the before :each do statement - it never "logs" the "user" in - thus I cannot test the controllers functionality of, is a session properly created?
Now most would say, use capybara and test it through that way - but that's wrong, IMO - sure if I'm doing front end testing that would work, but I'm testing controller based logic. Can some one tell me why this isn't working? routing works fine.
My puts session[:user_id] is coming up nil, when it shouldn't
let is lazily evaluated, even for the before clause, so the user has not been created as of the time you do the post to login. If you change to using let!, you'll avoid this problem.
You misunderstood SessionsController and RegistrationsController.
A Session is for an user who has already registered, not for creating an user. #create in SessionController means to create a session, not an user.
RegistrationController is for creating user with full details including password_confirmation.
To test SessionsController, you need to create a valid user in FactoryGirl at first, then use his credentials say email and password to sign in.

How to test Mass Assignment in Rails 4 using RSpec

Given a simple User model, in Rails 4 with name, email, and an admin boolean, what's the best approach to testing mass assignment using RSpec?
Here's the UsersController:
def create
#user = User.new user_params
...snip
end
private
def user_params
params.require(:user).permit(:name, :email)
end
and two different tries at testing this:
in user_spec.rb
describe "accessible attributes" do
describe "should not allow access to admin" do
before do
#user.admin = "1"
#user.save
end
it { should_not be_admin }
end
end
or in users_controller_spec.rb
it 'should only allow name and email to be set' do
#controller.user_params.keys.should eq(['name', 'email')
end
Neither work - the former just creates a user with admin set to true (failing the test) - presumably this bypasses strong_parameters. The latter works, but only if the user_params method is not private. (The official docs recommend setting it to private. Note - watching for a MassAssignment error in the user_spec doesn't work either (no error is raised).
Note - actually setting the user to admin in a view correctly works - the admin attribute is filtered out and all is happy, but would really like to see this working properly in a test.
An alternative suggest is to use the shoulda-matchers gem with the user_spec.rb:
describe User do
...
it { should_not allow_mass_assignment_of(:admin) }
...
end
(this doesn't work either), giving:
Failure/Error: it { should_not allow_mass_assignment_of(:admin) }
NoMethodError:
undefined method `active_authorizer' for #<Class:0x007f93c9840648>
(I assume this error is due to the fact shoulda-matchers isn't Rails 4 compatible yet).
Thanks in advance!
it "should not allow mass assignment" do
raw_parameters = { :admin => 1 }
parameters = ActionController::Parameters.new(raw_parameters)
expect {#user.update_attributes(parameters)}.should raise_error
end
In order to test mass assignment you should simulate passing parameters from controller.
https://github.com/rails/strong_parameters#use-outside-of-controllers

Rails 3.1, RSpec: testing model validations

I have started my journey with TDD in Rails and have run into a small issue regarding tests for model validations that I can't seem to find a solution to. Let's say I have a User model,
class User < ActiveRecord::Base
validates :username, :presence => true
end
and a simple test
it "should require a username" do
User.new(:username => "").should_not be_valid
end
This correctly tests the presence validation, but what if I want to be more specific? For example, testing full_messages on the errors object..
it "should require a username" do
user = User.create(:username => "")
user.errors[:username].should ~= /can't be blank/
end
My concern about the initial attempt (using should_not be_valid) is that RSpec won't produce a descriptive error message. It simply says "expected valid? to return false, got true." However, the second test example has a minor drawback: it uses the create method instead of the new method in order to get at the errors object.
I would like my tests to be more specific about what they're testing, but at the same time not have to touch a database.
Anyone have any input?
CONGRATULATIONS on you endeavor into TDD with ROR I promise once you get going you will not look back.
The simplest quick and dirty solution will be to generate a new valid model before each of your tests like this:
before(:each) do
#user = User.new
#user.username = "a valid username"
end
BUT what I suggest is you set up factories for all your models that will generate a valid model for you automatically and then you can muddle with individual attributes and see if your validation. I like to use FactoryGirl for this:
Basically once you get set up your test would look something like this:
it "should have valid factory" do
FactoryGirl.build(:user).should be_valid
end
it "should require a username" do
FactoryGirl.build(:user, :username => "").should_not be_valid
end
Here is a good railscast that explains it all better than me:
UPDATE: As of version 3.0 the syntax for factory girl has changed. I have amended my sample code to reflect this.
An easier way to test model validations (and a lot more of active-record) is to use a gem like shoulda or remarkable.
They will allow to the test as follows:
describe User
it { should validate_presence_of :name }
end
Try this:
it "should require a username" do
user = User.create(:username => "")
user.valid?
user.errors.should have_key(:username)
end
in new version rspec, you should use expect instead should, otherwise you'll get warning:
it "should have valid factory" do
expect(FactoryGirl.build(:user)).to be_valid
end
it "should require a username" do
expect(FactoryGirl.build(:user, :username => "")).not_to be_valid
end
I have traditionally handled error content specs in feature or request specs. So, for instance, I have a similar spec which I'll condense below:
Feature Spec Example
before(:each) { visit_order_path }
scenario 'with invalid (empty) description' , :js => :true do
add_empty_task #this line is defined in my spec_helper
expect(page).to have_content("can't be blank")
So then, I have my model spec testing whether something is valid, but then my feature spec which tests the exact output of the error message. FYI, these feature specs require Capybara which can be found here.
Like #nathanvda said, I would take advantage of Thoughtbot's Shoulda Matchers gem. With that rocking, you can write your test in the following manner as to test for presence, as well as any custom error message.
RSpec.describe User do
describe 'User validations' do
let(:message) { "I pitty da foo who dont enter a name" }
it 'validates presence and message' do
is_expected.to validate_presence_of(:name).
with_message message
end
# shorthand syntax:
it { is_expected.to validate_presence_of(:name).with_message message }
end
end
A little late to the party here, but if you don't want to add shoulda matchers, this should work with rspec-rails and factorybot:
# ./spec/factories/user.rb
FactoryBot.define do
factory :user do
sequence(:username) { |n| "user_#{n}" }
end
end
# ./spec/models/user_spec.rb
describe User, type: :model do
context 'without a username' do
let(:user) { create :user, username: nil }
it "should NOT be valid with a username error" do
expect(user).not_to be_valid
expect(user.errors).to have_key(:username)
end
end
end

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