I'm trying to keep my specs DRY by creating a shared example group that performs the boilerplate checks for all admin controllers (all controllers under the Admin namespace of my project). I'm struggling to figure out how to do it, since the shared example needs providing with the information about what actions and parameters to use. It should ideally present meaningful errors if a test fails (i.e. include the details of the action it was testing).
require 'spec_helper'
shared_examples "an admin controller" do
before(:each) do
#non_admin = User.make
#admin = User.make(:admin)
end
context "as an admin user" do
#actions.each do |action, params|
specify "I should be able to access ##{action.last} via #{action.first}" do
self.active_user = #admin
send(action.first, action.last, params)
response.status.should be_ok
end
end
end
context "as a regular user" do
#actions.each do |action, params|
specify "I should be denied access to ##{action.last}" do
self.active_user = #non_admin
send(action.first, action.last, params)
response.status.should be 403
end
end
end
end
describe Admin::UserNotesController do
#user = User.make
#actions = { [:get, :index] => { :user_id => #user.id },
[:get, :new] => { :user_id => #user.id },
[:post, :create] => { :user_id => #user.id } }
it_behaves_like "an admin controller"
end
This errors for the obvious reason that #actions is not visible to the shared example group. If I use let, this is only available in the context of an example, not in the context of the describe block. Any ideas?
Here's a much cleaner way that should work:
require 'spec_helper'
shared_examples "an admin controller" do |actions|
context "as an admin user" do
actions.each_pair do |action, verb|
specify "I should be able to access ##{action} via #{verb}" do
send(verb, action, :user_id => User.make(:admin).id)
response.status.should be_ok
end
end
end
context "as a regular user" do
actions.each_pair do |action, verb|
specify "I should be denied access to ##{action}" do
send(verb, action, :user_id => User.make.id)
response.status.should be 403
end
end
end
end
describe Admin::UserNotesController do
it_behaves_like "an admin controller", {
:index => :get,
:new => :get,
:create => :post
}
end
See http://relishapp.com/rspec/rspec-core/v/2-6/dir/example-groups/shared-examples for more information
Related
I'm trying to add a function to allow for quick testing of redirects for unauthenticated users. Here's what I have so far:
def unauthenticated_redirects_to redirect_path #yeild
context "when not signed in" do
it "redirects to #{redirect_path}" do
yield
expect(response).to redirect_to redirect_path
end
end
end
describe SomeController do
describe 'GET #show' do
unauthenticated_redirects_to('/some_path') { get :show }
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #whatever' do
unauthenticated_redirects_to('/some_other_path') { get :whatever }
end
end
This doesn't work, however, since the scope and context of the primary describe block is not available to the block passed to unauthenticated_redirects_to. This reasonably leads to the error: undefined method `get' for RSpec::Core::ExampleGroup::Nested_1::Nested_2:Class.
Is there a way around this or is there a cleaner way to accomplish something similar which I should consider?
Here's an approach using shared examples which triggers the example based on shared metadata (:auth => true in this case) and which parses the example group description to pick up some key parameters.
require 'spec_helper'
class SomeController < ApplicationController
end
describe SomeController, type: :controller do
shared_examples_for :auth => true do
it "redirects when not signed in" do
metadata = example.metadata
description = metadata[:example_group][:description_args][0]
redirect_path = metadata[:failure_redirect]
http_verb = description.split[0].downcase.to_s
controller_method = description.match(/#(.*)$/)[1]
send(http_verb, controller_method)
expect(response).to redirect_to redirect_path
end
end
describe 'GET #show', :auth => true, :failure_redirect => '/some_path' do
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #whatever', :auth => true, :failure_redirect => '/some_other_path' do
end
end
For completeness, here's another shared examples approach, this time using a block parameter with a before call which avoids the original scope problem:
require 'spec_helper'
class SomeController < ApplicationController
end
describe SomeController, type: :controller do
shared_examples_for 'auth ops' do
it "redirects when not signed in" do
expect(response).to redirect_to redirect_path
end
end
describe 'GET #show' do
it_behaves_like 'auth ops' do
let(:redirect_path) {'/some_path'}
before {get :show}
end
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #new' do
it_behaves_like 'auth ops' do
let(:redirect_path) {'/some_other_path'}
before {get :whatever}
end
end
end
Have a look at rspec shared example.
Using shared_examples_for seemed like overkill given that I was only concerned with a single example. Furthermore, it_behaves_like("unauthenticated redirects to", '/some_other_path', Proc.new{ get :whatever}) seems unnecessarily verbose. The trick is to use #send() to maintain the proper scope.
def unauthenticated_redirects_to path, method_action
context "when not signed in" do
it "redirects to #{path} for #{method_action}" do
send(method_action.first[0], method_action.first[1])
expect(response).to redirect_to path
end
end
end
describe 'GET #new' do
unauthenticated_redirects_to '/path', :get => :new
end
Where should I test authorization with RSpec?
When you create a Rails application with RSpec, there are three folders that seems to be adequate:
spec/routing
spec/requests
spec/controllers
In which one should I test if the user is logged in? Should I test in more than one spec type?
There is a subtle distinction to your question. Authorization usually referrers to permissions that users have within the app. Authentication referrers to users signing up and logging in users.
As far as Authentication goes, I usually prefer to use integration/requests specs or acceptance/feature specs. Feature specs are preferred lately because the Capybara DSL (page and visit) are only available in feature specs. They used to be allowed in request specs until the 2.x upgrade.
I will test things like signing up, signing in and signing out. For example,
# signing_up_spec.rb
feature 'Signing up' do
scenario 'Successful sign up' do
visit '/'
within 'nav' do
click_link 'Sign up'
end
fill_in "Email", :with => "user#ticketee.com"
fill_in "Password", :with => "password"
fill_in "Password confirmation", :with => "password"
click_button "Sign up"
page.should have_content("Please open the link to activate your account.")
end
end
This allows you to test the higher level aspect and lets you see the different components (controllers, views, etc...) within your app working together. This is by definition an integration/acceptance test. I would do the same as above for signing_in_spec.rb and signing_out_spec.rb
Now for Authorization, I would choose to use controller specs. This allows you to test the individual actions a user has permission to access. These controller specs are more granular in nature and are by definition unit/function tests. For example, say you had a ticket resource and you want to test that only certain users can access some particular functionality
# tickets_controller_spec.rb
describe TicketsController do
let(:user) { FactoryGirl.create(:confirmed_user) }
let(:project) { FactoryGirl.create(:project) }
let(:ticket) { FactoryGirl.create(:ticket, :project => project,
:user => user) }
context "standard users" do
it "cannot access a ticket for a project" do
sign_in(:user, user)
get :show, :id => ticket.id, :project_id => project.id
response.should redirect_to(root_path)
flash[:alert].should eql("The project you were looking for could not be found.")
end
context "with permission to view the project" do
before do
sign_in(:user, user)
define_permission!(user, "view", project)
end
def cannot_create_tickets!
response.should redirect_to(project)
flash[:alert].should eql("You cannot create tickets on this project.")
end
def cannot_update_tickets!
response.should redirect_to(project)
flash[:alert].should eql("You cannot edit tickets on this project.")
end
it "cannot begin to create a ticket" do
get :new, :project_id => project.id
cannot_create_tickets!
end
it "cannot create a ticket without permission" do
post :create, :project_id => project.id
cannot_create_tickets!
end
it "cannot edit a ticket without permission" do
get :edit, { :project_id => project.id, :id => ticket.id }
cannot_update_tickets!
end
it "cannot update a ticket without permission" do
put :update, { :project_id => project.id,
:id => ticket.id,
:ticket => {}
}
cannot_update_tickets!
end
it "cannot delete a ticket without permission" do
delete :destroy, { :project_id => project.id, :id => ticket.id }
response.should redirect_to(project)
flash[:alert].should eql("You cannot delete tickets from this project.")
end
it "can create tickets, but not tag them" do
Permission.create(:user => user, :thing => project, :action => "create tickets")
post :create, :ticket => { :title => "New ticket!",
:description => "Brand spankin' new",
:tag_names => "these are tags"
},
:project_id => project.id
Ticket.last.tags.should be_empty
end
end
end
end
I have found that the combination of rspec-rails, capybara and factory_girl_rails have served well in both types of testing within rails apps.
The examples above were taken from the Rails3Book repo on github. Take a look at the repo for more examples. It is a great way to see the possibilities you have when testing a rails app.
I've been learning Rails 3 with Devise and, so far, seem to have it working quite well. I've got custom session & registration controllers, recaptcha is working and a signed-in user can upload an avatar via carrierwave, which is saved on S3. Pretty happy with my progress.
Now I'm writing Rspec tests. Not going so well! I have a reasonable User model test, but that's because I found it online (https://github.com/RailsApps/rails3-devise-rspec-cucumber/) and was able to add to it by following Michael Hartl's excellent "Ruby on Rails 3 Tutorial".
My real problem is controller test and integration tests, especially controller tests. Initially I thought I'd be able to convert the tests in Michael's book, and I have to a small degree, but it's slow progress and I seem to be constantly hitting my head against a brick wall - partly, I think, because I don't know Rspec and capybara so well (have made some very dumb mistakes) but also because I don't really understand Devise well enough and am wondering if Devise plays as nicely as it might with Rspec; I read somewhere that, because Devise is Rack based, it might not always work as one might expect with Rspec. Don't know if that's true or not?
I know some people will wonder why this might be necessary since Devise is a gem and therefore already tested but I've had a couple of instances where changes elsewhere have broken login or registration without me immediately realizing. I think a good set of controller & integration tests would have solved this.
If I was able to do this myself I would and I'd publish it for others but, so far, writing these tests has been extremely painful and I really need to move on to other things.
I'm sure I wouldn't be the only one who could use this. Anyone know of a such a suite of tests?
In response to Jesse's kind offer of help...
Here is my registrations_controller_spec. The comments in "should render the 'edit' page" show the sort of things I am struggling with. Also, "should create a user" has some things I've tried to test but not been able to:
require File.dirname(__FILE__) + '/../spec_helper'
describe Users::RegistrationsController do
include Devise::TestHelpers
fixtures :all
render_views
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
end
describe "POST 'create'" do
describe "failure" do
before(:each) do
#attr = { :email => "", :password => "",
:password_confirmation => "", :display_name => "" }
end
it "should not create a user" do
lambda do
post :create, :user_registration => #attr
end.should_not change(User, :count)
end
it "should render the 'new' page" do
post :create, :user_registration => #attr
response.should render_template('new')
end
end
describe "success" do
before(:each) do
#attr = { :email => "user#example.com",
:password => "foobar01", :password_confirmation => "foobar01", :display_name => "New User" }
end
it "should create a user" do
lambda do
post :create, :user => #attr
response.should redirect_to(root_path)
#response.body.should have_selector('h1', :text => "Sample App")
#response.should have_css('h1', :text => "Sample App")
#flash[:success].should == "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
#response.should have_content "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
end.should change(User, :count).by(1)
end
end
end
describe "PUT 'update'" do
before(:each) do
#user = FactoryGirl.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
describe "Failure" do
before(:each) do
# The following information is valid except for display_name which is too long (max 20 characters)
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
end
it "should render the 'edit' page" do
put :update, :id => subject.current_user, :user => #attr
# HAVE PUT THE DEBUGS THAT I'D LIKE TO GET WORKING FIRST
# Would like to be able to debug and check I'm getting the error(s) I'm expecting
puts subject.current_user.errors.messages # doesn't show me the errors
# Would like to be able to debug what html is being returned:
puts page.html # only return the first line of html
# Would like to be able to determine that this test is failing for the right reasons
response.should have_content "Display name is too long (maximum is 20 characters)" # doesn't work
response.should render_template('edit')
end
end
describe "Success" do
it "should change the user's display name" do
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
put :update, :id => subject.current_user, :user => #attr
subject.current_user.reload
response.should redirect_to(root_path)
subject.current_user.display_name == #attr[:display_name]
end
end
end
describe "authentication of edit/update pages" do
describe "for non-signed-in users" do
before(:each) do
#user = FactoryGirl.create(:user)
end
describe "for non-signed-in users" do
it "should deny access to 'edit'" do
get :edit, :id => #user
response.should redirect_to(new_user_session_path)
end
it "should deny access to 'update'" do
put :update, :id => #user, :user => {}
response.should redirect_to(new_user_session_path)
end
end
end
end
end
Acording to Devise Wiki, controller tests have to be some kind of hibrid (unit + integration) tests. You have to create an instance of User (or you auth entity) in the database. Mocking Devise stuff is really hard, trust me.
Try this: How-To:-Controllers-and-Views-tests-with-Rails-3-and-rspec
With Cucumber, you can create a step that already do the login process.
Hope it helps.
OK, so a couple of things to make this work:
Here's how you should test that the body have specific text (you were missing the .body
response.body.should include "Display name is too long (maximum is 30 characters)"
You could also test that the #user has an error:
assigns[:user].errors[:display_name].should include "is too long (maximum is 30 characters)"
But, you do need to actually make the name be over 20/30 characters long, so I changed the attributes being posted to:
#attr = { :email => #user.email, :display_name => ("t" * 35), :current_password => #user.password }
https://gist.github.com/64151078628663aa7577
I'm testing my controllers using Rspec and I can't seem to set the session variable of the current controller under test before making the request to the path.
For example this works:
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
get controller_path
request.session[:state] = "12334"
end
end
This doesn't work (i get an error saying session is not a method of Nil class):
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
request.session[:state] = "12334"
get controller_path
end
end
Any ideas?
With new version of RSpec this is done pretty nice, look:
describe SessionController do
# routes are mapped as:
# match 'login' => 'session#create'
# get 'logout' => 'session#destroy'
describe "#create" do
context "with valid credentials" do
let :credentials do
{ :email => 'example#gmail.com', :password => 'secret' }
end
let :user do
FactoryGirl.create(:user, credentials)
end
before :each do
post '/login', credentials
end
it "creates a user session" do
session[:user_id].should == user.id
end
end
# ...
end
describe "#destroy" do
context "when user logged in" do
before :each do
get "/logout", {}, { :user_id => 123 } # the first hash is params, second is session
end
it "destroys user session" do
session[:user_id].should be_nil
end
# ...
end
end
end
You can also use simply request.session[:user_id] = 123 inside before(:each) block, but above looks pretty nicer.
Try this:
describe "GET /controller/path" do
it "if not matching CRSF should display message" do
session[:state] = "12334"
get controller_path
end
end
I was wondering if i could have some feedbacks with the controller spec bellow. In fact i'm new when writing specs and controller's spec are way different from model's spec ! So i'm wondering if i may not go in the wrong direction...
subjects_controller.rb
def show
#subject = Subject.find(params[:id])
if #subject.trusted?(current_user)
#messages = #subject.messages
else
#messages = #subject.messages.public
#messages = #messages + #subject.messages.where(:user_ids => current_user.id)
#messages.uniq!
end
# sort the list
#messages = #messages.sort_by(&:created_at).reverse
if !#subject.company.id == current_user.company.id
redirect_to(subjects_path, :notice => "Invalid subject")
end
end
subjects_controller_spec.rb
require 'spec_helper'
describe SubjectsController do
before(:each) do
#subject = mock_model(Subject)
end
context "for signed users" do
before(:each) do
#current_user = sign_in Factory(:user)
end
context "GET #show" do
before(:each) do
Subject.stub!(:find, #subject).and_return(#subject)
end
context "when current_user is trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(true)
#subject.should_receive(:messages).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when current_user is not trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(false)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:public).and_return(messages)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:where).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when subject's company is not equal to current_user's company" do
# I have no idea of how to implement ==
end
end
end
end
Factories.rb
Factory.define :user do |u|
u.first_name 'Test User' #
u.username 'Test User' #
u.surname 'TheTest' #
u.email 'foo#foobar.com' #
u.password 'please' #
u.confirmed_at Time.now #
end
As far as I can tell you're on the right path. The basic idea is to completely isolate your controller code from model and view in these tests. You appear to be doing that--stubbing and mocking model interaction.
Don't write RSpec controller specs at all. Use Cucumber stories instead. Much easier, and you get better coverage.