RSpec: testing cancan with home-spun authentication - ruby-on-rails

This is a simple question, it's just that I'm still wrapping my head around RSpec's syntax & methodology...so I'm a bit confused. Please bear with me..I'm wondering how to approach testing controllers (and requests, etc.) when Cancan is involved and users can have different levels of authorization.
I've read that one advantage to Cancan is that it keeps all Auth in one model...great! but it seems to have confused testing for me...so I'm looking for advice.
Background: I'm using a bitmask to hold the authorization level for the user (a la Railscast #189). So the integer column roles_mask will hold all the auth for a user. Not sure this matters but now you know.
I'd like to have some tests as a guest user that should fail if guest tries to create a Post. But then in same posts_controller_spec I'd like to test that an admin and moderator can create the same post. Just not sure how best to set this up.
So, should I have a factory for a guest user, one for an admin, etc... Moving #user.roles_mask = 1 to a describe block before specs I want for an "admin" user doesn't work. No method errors. But, if I assign the roles_mask in the Factory it does work?!? So how can I test for varying levels of roles_mask? (nil, 0, 1, 2, 4, etc?)
describe PostsController do
before(:each) do
#user = Factory(:user)
session[:user_id] = #user.id
#attr = Factory.attributes_for(:post)
end
describe "GET index" do
it "assigns all posts as #posts" do
post = #user.posts.create(#attr)
get :index
assigns(:posts).should eq([post])
end
end
...
describe "POST create" do
describe "with valid params" do
it "creates a new Post" do
expect {
post :create, :post => #attr
}.to change(Post, :count).by(1)
end
...
end
# posts_controller.rb
class PostsController < ApplicationController
before_filter :login_required, :except => [:index, :show]
load_and_authorize_resource
...
end
Factory:
Factory.define :user do |user|
user.sequence(:email) { |n| "foo#{n}#dummycorp.com" }
user.password "foobar"
user.password_confirmation { |u| u.password }
user.firstname "foo"
user.roles_mask 1 # < --- remove & tests fail
# < --- how to manipulate dynamically in tests
end

I think you are confused (as I was) by the fact that the controller is protecting access to resources - but it's still the ability class that determines whether the controller's actions can be carried out on a particular resource. Whether you access the resources through the PostsController or some other controller, should really make no difference - you still don't want guests making posts. If you really want to be certain that load_and_authorize_resource is being called in the PostsController you could set up a single functional test for that (guest create post should fail) - then the ability tests should confirm the detail.
require "cancan/matchers"
describe Ability do
before(:each) do
end
[Contact, Question, Provider, Organisation].each do |model|
it "should allow any user to read a #{model.to_s} details but not delete them" do
user= Factory(:user)
ability = Ability.new(user)
ability.should be_able_to(:show, model.new)
ability.should_not be_able_to(:delete, Factory(model.to_s.underscore.to_sym))
end
end
[Contact, Organisation].each do |model|
it "should allow any admin user to delete a #{model.to_s} " do
user= Factory(:admin)
ability = Ability.new(user)
ability.should be_able_to(:delete, Factory.build(model.to_s.underscore.to_sym))
end
end
etc
In my case I have admin users and normal users with different abilities - I just created one of each in turn, and called the relevant methods on the models I wanted to restrict access to. I didn't actually test the controller much at all because load_and_authorize_resource is called in my case in the application_controller and so a single test makes sure it applies throughout the app

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."

How to test cancancan abilities in controller test with default testing framework (minitest)

In my rails app I have two user roles: 'student' and 'admin'. They have different access authorization to different pages, e.g., 'admin' can access listing users page (index) but 'student' cannot. This is controlled using cancancan.
Now I am writing tests for controllers and, since I have two different roles, (to my knowledge) I need two separate tests for a single behaviour, for example:
test "should get index for admin" do
sign_in(users(:admin))
get "index"
assert_response :success
end
test "should not get index for student" do
sign_in(users(:student))
get "index"
assert_response :unauthorized
end
where sign_in(*) is the method for handing user login (sessions etc.)
As I am considering adding more roles (e.g. 'manager', 'agent'), I need to add new tests for all the controller methods each time I add a role. This is tedious and not "DRY", so I am trying to find a more elegant way to handle this. Here is my first thought:
In my test_helper.rb, I added:
def assert_admin_only(&block)
sign_in(users(:admin))
instance_exec(&block)
assert_response :success
sign_out
sign_in(users(:student))
instance_exec(&block)
assert_response :unauthorized
sign_out
sign_in(users(:agent))
instance_exec(&block)
assert_response :unauthorized
sign_out
end
Then in my test:
test "should get index for admin only" do
assert_admin_only do
get "index"
end
end
So that each time I added a new role, I only have to add a few lines in the test_helper.rb method in order to test the abilities.
However, it does not work as I thought because "Functional tests allow you to test a single controller action per test method." according to Rails API DOC, while in my code I was firing two actions or even more. For some reason I can't figure out, it seems that the sign_in and sign_out don't actually change the current_user (although they are working perfectly in real requests), which essentially make my attempt fail.
In a word, I want to reuse my tests for different user roles so that I don't have to waste my time copying and pasting existing codes every time I add a new role. I would really appreciate it if you guys could provide some brilliant ideas.
for example:
require 'test_helper'
class ArticlesControllerTest < ActionController::TestCase
include Devise::TestHelpers
setup do
#article = articles(:one)
#admin = users(:admin)
#expert = users(:expert)
#user = users(:emelya)
#student = users(:student)
end
test "should get index if admin" do
sign_in #admin
ability = Ability.new(#admin)
assert ability.can? :index, Article
get :index
assert_response :success
assert_not_nil assigns(:articles)
end
test "should not get index for other users" do
[#expert, #user, #student] do |user|
sign_in user
ability = Ability.new(user)
assert ability.cannot? :index, Article
assert_raise CanCan::AccessDenied do
get :index
end
sign_out user
end
end
end

Mocking and stubbing in testing

I've recently learned how to stub in rspec and found that some benefits of it are we can decouple the code (eg. controller and model), more efficient test execution (eg. stubbing database call).
However I figured that if we stub, the code can be tightly tied to a particular implementation which therefore sacrifice the way we refactor the code later.
Example:
UsersController
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
User.create(name: params[:name])
end
end
Controller spec
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
By doing that didn't I just limit the implementation to only using User.create? So later if I change the code my test will fail even though the purpose of both code is the same which is to save the new user to database
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new
#user.name = params[:name]
#user.save!
end
end
Whereas if I test the controller without stubbing, I can create a real record and later check against the record in the database. As long as the controller is able to save the user Like so
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
post :create, :name => "abc"
user = User.first
expect(user.name).to eql("abc")
end
end
end
Really sorry if the codes don't look right or have errors, I didn't check the code but you get my point.
So my question is, can we mock/stub without having to be tied to a particular implementation? If so, would you please throw me an example in rspec
You should use mocking and stubbing to simulate services external to the code, which it uses, but you are not interested in them running in your test.
For example, say your code is using the twitter gem:
status = client.status(my_client)
In your test, you don't really want your code to go to twitter API and get your bogus client's status! Instead you stub that method:
expect(client).to receive(:status).with(my_client).and_return("this is my status!")
Now you can safely check your code, with deterministic, short running results!
This is one use case where stubs and mocks are useful, there are more. Of course, like any other tool, they may be abused, and cause pain later on.
Internally create calls save and new
def create(attributes = nil, options = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
object = new(attributes, options, &block)
object.save
object
end
end
So possibly your second test would cover both cases.
It is not straight forward to write tests which are implementation independent. That's why integration tests have a lot of value and are better suited than unit tests for testing the behavior of the application.
In the code you're presented, you're not exactly mocking or stubbing. Let's take a look at the first spec:
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
Here, you're testing that User received the 'create' message. You're right that there's something wrong with this test because it's going to break if you change the implementation of the controllers 'create' action, which defeats the purpose of testing. Tests should be flexible to change and not a hinderance.
What you want to do is not test implementation, but side effects. What is the controller 'create' action supposed to do? It's supposed to create a user. Here's how I would test it
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect { post :create, name: 'abc' }.to change(User, :count).by(1)
end
end
end
As for mocking and stubbing, I try to stay away from too much stubbing. I think it's super useful when you're trying to test conditionals. Here's an example:
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
flash[:success] = 'User created'
redirect_to root_path
else
flash[:error] = 'Something went wrong'
render 'new'
end
end
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it "renders new if didn't save" do
User.any_instance.stub(:save).and_return(false)
post :create, name: 'abc'
expect(response).to render_template('new')
end
end
end
Here I'm stubbing out 'save' and returning 'false' so I can test what's supposed to happen if the user fails to save.
Also, the other answers were correct in saying that you want to stub out external services so you don't call on their API every time you're running your test suite.

Is there a better way to test this Rails controller with RSpec?

I have an Account model that validates that its subdomain is unique.
I'm trying to learn how to test controllers with RSpec.
Here's what I've come up with, but it's quite different than the generated RSpec test and i'm wondering if this is a good way to test this or if there's a better way.
My test:
describe "POST create" do
describe "with valid params" do
it "creates a new Account" do
original_count = Account.count
account = FactoryGirl.build(:account, :subdomain => 'newdomain')
post :create, {:account => account}
account.save!
new_count = Account.count
expect(new_count).to eq(original_count + 1)
end
...
EDIT
I forgot to point out the fact that in my spec_helper I have the code below. It is needed because of the way i'm handling subdomains:
config.before(:each, :type => :controller) do
#account = FactoryGirl.create(:account)
#user = FactoryGirl.create(:user)
#request.host = "#{#account.subdomain}.example.com"
sign_in #user
end
There is, by using Rspec's expect to change, and FactoryGirl's attributes_for (might need tweaking, not tested):
describe "POST create" do
describe "with valid params" do
it "creates a new Account" do
expect{
post :create, { account: attributes_for(:account) }
}.to change{Account.count}.by(1)
end
...
Validate your unique subdomain constraint in your unit tests, perhaps using shoulda-matchers:
describe Account do
it { should validate_uniqueness_of(:subdomain) }
end
I would make a controller spec a true unit test and not involve the database. Something like:
describe AccountsController do
describe '#create' do
it "creates a new Account" do
account_attrs = FactoryGirl.attributes_for :account
expect(Account).to receive(:create!).with account_attrs
post :create, account: account_attrs
end
end
end
I'd also have a feature spec (or a Cucumber scenario) that integration-tested the entire interaction of which the post to AccountsController was a part. Actually if I had a happy-path feature spec/scenario there would be no need to write the controller spec above, but I'd need controller specs for error paths (like trying to create an Account with the same subdomain as an existing Account) and other variations and I'd write them similarly to the spec above, by stubbing and mocking database calls.

rail3/rspec/devise: rspec controller test fails unless I add a dummy=subject.current_user.inspect

I'm trying to get RSpec working for a simple scaffolded app, starting with the rspec scaffold tests.
Per the devise wiki, I have added the various devise config entries, a factory for a user and an admin, and the first things I do in my spec controller is login_admin.
Weirdest thing, though... all my specs fail UNLESS I add the following statement right after the it ... do line:
dummy=subject.current_user.inspect
(With the line, as shown below, the specs pass. Without that line, all tests fail with the assigns being nil instead of the expected value. I only happened to discover that when I was putting some puts statements to see if the current_user was being set correctly.)
So what it acts like is that dummy statement somehow 'forces' the current_user to be loaded or refreshed or recognized.
Can anyone explain what's going on, and what I should be doing differently so I don't need the dummy statement?
#specs/controllers/brokers_controller_spec.rb
describe BrokersController do
login_admin
def valid_attributes
{:name => "Bill", :email => "rspec_broker#example.com", :company => "Example Inc", :community_id => 1}
end
def valid_session
{}
end
describe "GET index" do
it "assigns all brokers as #brokers" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :index, {}, valid_session
assigns(:brokers).should eq([broker])
end
end
describe "GET show" do
it "assigns the requested broker as #broker" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :show, {:id => broker.to_param}, valid_session
assigns(:broker).should eq(broker)
end
end
and per the devise wiki here is how I login a :user or :admin
#spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
What a struggle! Thank you Robin, I've been googling on this for hours and finally saw your post; now my controller tests are working :)
To add to your answer, I figured out how to get the devise session into the valid_session hash, which allows the controller tests to run properly as generated by rails.
def valid_session
{"warden.user.user.key" => session["warden.user.user.key"]}
end
In your tests, there is the following code:
def valid_session
{}
end
...
get :index, {}, valid_session
Because of this 'session' variable, the "log_in" that you did is essentially not being used during the 'get'.
The way that I solved it was to remove all of the "valid_session" arguments to the get, post, put, delete calls in that controller's spec. The example above becomes:
get :index, {}
I suspect that there's a way to add the devise's session to the "valid_session" hash, but I don't know what it is.
Thanks for this solution.
If you are using a different Devise model, the session id also changes.
For a model Administrator use the following:
def valid_session
{'warden.user.administrator.key' => session['warden.user.administrator.key']}
end

Resources