How to test after_destroy callback in Rails and RSpec? - ruby-on-rails

I have this User class in Ruby on Rails:
class User < ActiveRecord::Base
after_destroy :ensure_an_admin_remains
private
def ensure_an_admin_remains
if User.where("admin = ?", true).count.zero?
raise "Can't delete Admin."
end
end
end
This works great and causes a database rollback if someone accidentally deletes an admin user.
The problem is that it seems to break the user delete action, even when testing with a non-admin user (generated by Factory Girl). This is my user_controller_spec.rb:
describe 'DELETE #destroy' do
before :each do
#user = create(:non_admin_user)
sign_in(#user)
end
it "deletes the user" do
expect{
delete :destroy, id: #user
}.to change(User, :count).by(-1)
end
end
Whenever I run this test, I get this error:
Failure/Error: expect{
count should have been changed by -1, but was changed by 0
There shouldn't be any error, though, because #user's admin attribute is set to false by default.
Can anybody help me out here?
Thanks...

I may be wrong but,
Your spec start with empty database right? So there is no admin user present in your db.
So when you call delete, you'll always have User.where("admin = ?", true).count equal to zero
Try creating an user admin before your test
describe 'DELETE #destroy' do
before :each do
create(:admin_user)
#user = create(:non_admin_user)
sign_in(#user)
end
it "deletes the user" do
expect{
delete :destroy, id: #user
}.to change(User, :count).by(-1)
end
end

I would make the following change:
before_destroy :ensure_an_admin_remains
def ensure_an_admin_remains
if self.admin == true and User.where( :admin => true ).count.zero?
raise "Can't delete Admin."
end
end

An alternative is to make the called function ensure_an_admin_remains a public function, such as check_admin_remains.
You can then test, the logic of check_admin_remains as if it were any other function.
Then in another test, you can ensure that function is called on destroy without any database interaction as follows:
let(:user) { build_stubbed(:user) }
it 'is called on destroy' do
expect(user).to receive(:check_admin_remains)
user.run_callbacks(:destroy)
end

You shouldn't raise for control flow. You can halt during callbacks to prevent the record being commited.
I've improved one some of the answers here for anyone else trying to work out how to do this properly as of Rails 5
class User < ActiveRecord::Base
before_destroy :ensure_an_admin_remains
private def ensure_an_admin_remains
return unless admin && User.where(admin: true).limit(2).size == 1
errors.add(:base, "You cannot delete the last admin.")
throw :abort
end
end

Related

RSpec check if method has been called

I have an AccountsController and a destroy action. I want to test if the account is being deleted and if the subscription is being canceled.
AccountsController
def destroy
Current.account.subscription&.cancel_now!
Current.account.destroy
end
RSpec
describe "#destroy" do
let(:account) { create(:account) }
it "deletes the account and cancels the subscription" do
allow(account).to receive(:subscription)
expect do
delete accounts_path
end.to change(Account, :count).by(-1)
expect(account.subscription).to have_received(:cancel_now!)
end
end
But the above test does not pass. It says,
(nil).cancel_now!
expected: 1 time with any arguments
received: 0 times with any arguments
Because the account.subscription returns nil it is showing this. How do I fix this test?
You need to replace the account entity in the context of the controller to the account from the test
It could be
describe "#destroy" do
let(:account) { create(:account) }
it "deletes the account and cancels the subscription" do
allow(Current).to receive(:account).and_return(account)
# if the subscription does not exist in the context of the account
# then you should stub or create it...
expect do
delete accounts_path
end.to change(Account, :count).by(-1)
expect(account.subscription).to have_received(:cancel_now!)
end
end
Regarding subscription
expect(account).to receive(:subscription).and_return(instance_double(Subscription))
# or
receive(:subscription).and_return(double('some subscription'))
# or
create(:subscription, account: account)
# or
account.subscription = create(:subscription)
# or other options ...

Rspec: How to test a method that raises an error

I have a SubscriptionHandler class with a call method that creates a pending subscription, attempts to bill the user and then error out if the billing fails. The pending subscription is created regardless of whether or not the billing fails
class SubscriptionHandler
def initialize(customer, stripe_token)
#customer = customer
#stripe_token = stripe_token
end
def call
create_pending_subscription
attempt_charge!
upgrade_subscription
end
private
attr_reader :stripe_token, :customer
def create_pending_subscription
#subscription = Subscription.create(pending: true, customer_id: customer.id)
end
def attempt_charge!
StripeCharger.new(stripe_token).charge! #raises FailedPaymentError
end
def upgrade_subscription
#subscription.update(pending: true)
end
end
Here is what my specs look like:
describe SubscriptionHandler do
describe "#call" do
it "creates a pending subscription" do
customer = create(:customer)
token = "token-xxx"
charger = StripeCharger.new(token)
allow(StripeCharger).to receive(:new).and_return(charger)
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
handler = SubscriptionHandler.new(customer, token)
expect { handler.call }.to change { Subscription.count }.by(1) # Fails with FailedPaymentError
end
end
end
But this does not change the subscription count, it fails with the FailedPaymentError. Is there a way to check that the subscription count increases without the spec blowing up with FailedPaymentError.
You should be able to use Rspec compound expectations for this
https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations
So I'll re-write your expectation to something like this:
expect { handler.call }.
to raise_error(FailedPaymentError).
and change { Subscription.count }.by(1)
It can be done like this
expect{ handler.call }.to raise_error FailedPaymentError
Should work.
If you don't want to raise error at all then you can remove this line, and return a valid response instead
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
More info - How to test exception raising in Rails/RSpec?
Official RSpec docs
https://relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/expect-error

Undefined local variable or method params in Rspec

Hi I am implementing a method to delete a user account in my web application. My controller:
class UsersController < ApplicationController
before_filter :set_current_user
def user_params
params.require(:user).permit(:user_id, :first_name, :last_name, :email, :password, :password_confirmation)
end
def delete_account
#user = User.find_by_id(params[:id])
if #user.present?
#user.destroy
flash[:notice] = "User Account Deleted."
end
redirect_to root_path
end
def destroy
User.delete(:user_id)
redirect_to root_path
end
end
My rspec:
require 'spec_helper'
require 'rails_helper'
require'factory_girl'
describe UsersController do
describe "delete account" do
before :each do
#fake_results = FactoryGirl.create(:user)
end
it "should call the model method that find the user" do
expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
end
it "should destroy the user account from the database" do
expect{delete :destroy, id: #fake_results}.to change(User, :count).by(-1)
end
it "should redirect_to the home page" do
expect(response).to render_template(:home)
end
end
end
The first error is
Failure/Error: expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
NameError:undefined local variable or method `params' for #<RSpec::ExampleGroups::UsersController::DeleteAccount:0x00000007032e18>
I know what this error means but I don't know how to correct it. How can I pass the user id from the controller to rspec?
The second error is:
Failure/Error: expect(response).to render_template(:home)
expecting <"home"> but rendering with <[]>
I think there is something wrong with my controller method. It should redirect to the home page but it doesn't.
params is not available in your tests, it's available in your controller.
Looks like you create a test user in your test:
#fake_results = FactoryGirl.create(:user)
Then, you can use the id of this test user (#fake_results.id) instead of trying to use params[:id]:
expect(User).to receive(:find).with(#fake_results.id).and_return (#fake_results)
Although, you may want to change the name from #fake_results to something more meaningful e.g. test_user or so.
However, this should fix both of your problems as your second problem is there because of the first problem. As it's failing to delete the user in the first place, it's not being redirected to the root path and hence the home template is not rendering.

Rails response.should be_success is never true

I am following Michael Hartl's excellent tutorial on Ruby on Rails. I'm stuck trying to understand the way ActionDispatch::Response works. This derives from Exercise 9 of Chapter 9 (Rails version 3.2.3).
In particular we're asked to make sure that the admin user is unable to User#destroy himself. I have an idea how to do that, but since I'm trying to follow a TDD methodology, I'm first writing the tests.
This is the relevant snippet in my test:
describe "authorization" do
describe "as non-admin user" do
let(:admin) {FactoryGirl.create(:admin)}
let(:non_admin) {FactoryGirl.create(:user)}
before{valid_signin non_admin}
describe "submitting a DELETE request to the Users#destroy action" do
before do
delete user_path(admin)
#puts response.message
puts response.succes?
end
specify{ response.should redirect_to(root_path) }
specify{ response.should_not be_success }
end
end
#Exercise 9.6-9 prevent admin from destroying himself
describe "as admin user" do
let(:admin){FactoryGirl.create(:admin)}
let(:non_admin){FactoryGirl.create(:user)}
before do
valid_signin admin
end
it "should be able to delete another user" do
expect { delete user_path(non_admin) }.to change(User, :count).by(-1)
end
describe "can destroy others" do
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
describe "cannot destroy himself" do
before do
delete user_path(admin)
puts response.success?
end
#specify{response.should_not be_success}
specify{response.should be_redirect}
end
end
.
.
.
end
All the tests pass except the "can destroy others" test.
However, if I puts response.success? after every delete request, I always get False, so none of the requests "succeed".
Manually interacting with the webapp and deleting users works just fine, so I assume that response.success does not mean that the detroy(or whatever request for that matter) was not successful, but something else. I read it has to do with the difference between HTTP responses 200/302/400, but I'm not totally sure.
For the record, this is my User#destroy:
def destroy
User.find(params[:id]).destroy
flash[:success]="User destroyed."
redirect_to users_path
end
Any light on this?
thanks!
Edit
This is my factory:
FactoryGirl.define do
factory :user do
sequence(:name){ |n| "Person #{n}" }
sequence(:email){ |n| "person_#{n}#example.com"}
password "foobar"
password_confirmation "foobar"
factory :admin do
admin true
end
end
end
Edit 2 as suggested by #Peter Alfvin, I changed lines
let(:user){FactoryGirl.create(:user)}
to
let(:admin){FactoryGirl.create(:admin)}
And all user to admin in general. I also added a puts admin.admin? before the delete request. Still not working!
Edit 3
Changing the test "can destroy others" as:
describe "can destroy others" do
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
Does not seem to help either.
For your "admin" case, you're still creating and logging in as a "regular" user instead of an admin user, which is why you can't destroy anyone else.
response.success does indeed refer to the HTTP response code. By default, I believe this is anything in the 200 range. redirect_to is in the 300 range.
Make sure your user Factory includes this line
factory :user do
#your user factory code
factory :admin do
admin true
end
end
Then FactoryGirl.create(:admin) will return an admin user or you can also use user.toggle!(:admin) which will switch a standard user to an admin user.
try this then
describe "as admin user" do
let(:admin){FactoryGirl.create(:admin)}
let(:non_admin){FactoryGirl.create(:user)}
before do
valid_signin admin
end
it "should be able to delete another user" do
expect { delete user_path(non_admin) }.to change(User, :count).by(-1)
end
it "can destroy others" do #
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
it "cannot destroy himself" do
before do
delete user_path(admin)
puts response.success?
end
#specify{response.should_not be_success}
specify{response.should be_redirect}
end
end
describe creates a magic Class it becomes a subClass of the describe class from my understanding. Rails has a lot of this magic and it can get confusing. Also I have not seen your controller but what are you expecting to happen when you destroy a user because if you followed the tutorial then there will be a redirect delete sent through the browser will call your destroy method in the UsersController which in the tutorial has this line redirect_to users_url so response.should_not be_redirect will always fail because the spec is wrong not the controller.

why is my CanCan Ability class overly permissive?

I'm (finally) wiring CanCan / Ability into my app, and I've started by writing the RSpec tests. But they're failing — my Abilities appear to be overly permissive, and I don't understand why.
First, the Ability class. The intention is that non-admin users can manage only themselves. In particular, they cannot look at other users:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # create guest user if needed
if (user.has_role?(:admin))
can(:manage, :all)
else
can(:manage, User, :id => user.id)
end
end
end
The RSpec tests:
require 'spec_helper'
require 'cancan/matchers'
describe Ability do
before(:each) do
#user = User.create
end
describe 'guest user' do
before(:each) do
#guest = nil
#ability = Ability.new(#guest)
end
it "should_not list other users" do
#ability.should_not be_able_to(:read, User)
end
it "should_not show other user" do
#ability.should_not be_able_to(:read, #user)
end
it "should_not create other user" do
#ability.should_not be_able_to(:create, User)
end
it "should_not update other user" do
#ability.should_not be_able_to(:update, #user)
end
it "should_not destroy other user" do
#ability.should_not be_able_to(:destroy, #user)
end
end
end
All five of these tests fail. I've read the part of Ryan's documentation where he says:
Important: If a block or hash of
conditions exist they will be ignored
when checking on a class, and it will
return true.
... but at most, that would only explain two of the five failures. So clearly I'm missing something fundamental.
I would expect this to work:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # create guest user if needed
if (user.has_role?(:admin))
can(:manage, :all)
elsif user.persisted?
can(:manage, User, :id => user.id)
end
end
end
I'm not sure what the behavior is defined to be if you pass :id => nil, which is what happens in the guest case, but at any rate, if you don't want the guest to access the list view, you shouldn't call can :manage, User for that user at all.
In general, I find that assigning user ||= User.new to make the ability harder to reason about.
Hey, apparently this should work, but some refactoring would help you to find the issue:
require 'spec_helper'
require 'cancan/matchers'
describe Ability do
before(:each) { #user = User.create }
describe 'guest user' do
before(:each) { #ability = Ability.new(nil) }
subject { #ability } # take advantage of subject
it "should not be an admin user" do
#user.should_not be_admin
#user.should be_guest
end
it "should_not show other user" do
should_not be_able_to(:read, #user)
end
it "should_not create other user" do
should_not be_able_to(:create, User)
end
it "should_not update other user" do
should_not be_able_to(:update, #user)
end
it "should_not destroy other user" do
should_not be_able_to(:destroy, #user)
end
end
end
Note that also I removed this example #ability.should_not be_able_to(:read, User).
Hope it helps you.
I've got this bad habit of answering my own questions, but I give props to #jpemberthy and #Austin Taylor for pointing me in the right direction. First (and this is cosmetic), I added this to my User model:
class User
...
def self.create_guest
self.new
end
def guest?
uninitialized?
end
end
and cleaned up my Abilities model accordingly:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.create_guest
if (user.admin?)
<admin abilities here>
elsif (user.guest?)
<guest abilities here>
else
<regular user abilities here>
end
end
end
But the real fix was in my RSpec tests. Since User has validations on email and password fields, my original code of:
before(:each) do
#user = User.create
end
was failing, thus creating an uninitialized #user. Since the :id field was nil, the Ability clause:
can(:manage, User, :id => user.id)
was succeeding with a guest user because nil == nil (if that makes sense). Adding the required fields to satisfy the User validations made (almost) everything work.
Moral: just as #jpemberthy suggested in his code, always include a test to make sure your user objects have the privileges that they are supposed to! (I still have another question regarding CanCan, hopefully less boneheaded than this one, appearing in a StackOverflow topic near you...)

Resources