Reputation: 4787
In my app, I give access to a customer to HIS own deals thanks to Cancan.
It works when I try it "manually" with the browser but I fail at implementing the rspec tests. A customer can't access other customer's deals but only his own (the administrator give him access through Active Admin interface).
It's like I am not managing to make rspec understand that the customer(through FactoryGirl) I create for tests should be allowed/associated with the deals I create for the tests (again through FactoryGirl).
THE TEST: /spec/controllers/deals_controller_spec.rb
require 'spec_helper'
require "cancan/matchers"
describe DealsController do
context "As signed-in CUSTOMER" do
before do
@customer = FactoryGirl.create(:customer) #the factory builds a basic customer i.e with 'prospect role' attributed by default
@deal = FactoryGirl.create(:deal, :customers => [@customer]) # in array as a deal has_many customers
sign_in_customer @customer
end
describe "the customer can read=view the page of a Deal HE OWNS " do
it "can access the page" do
get :deal_page, { :id => @deal.id }
expect(current_path).to eq(deal_page_path(@deal))
page.should have_content('Here is one of your deals, dear customer')
end
end
end
Here is the error I get:
DealsController As signed-in CUSTOMER with access to the deal page
Failure/Error: expect(current_path).to eq(deal_page_path(@deal))
expected: "/deals_page/2"
got: "/customer_interface_homepage"
(compared using ==)
Here is the detailed test log
Deal Exists (0.8ms) SELECT 1 AS one FROM "deals" WHERE LOWER("deals"."deal_code") = LOWER('CHA1FR001') LIMIT 1
SQL (2.1ms) INSERT INTO "deals" ("admin_user_id", "client_contact_point_name", blabla") VALUES ($1, $2, blabla...) RETURNING "id" [["admin_user_id", 1], ["client_contact_point_name", "henri Cool"], ["client_contact_point_profile_url", "http://example.com"], ....blabla...]
(...blabla)
Customer Exists (0.6ms) SELECT 1 AS one FROM "customers" WHERE (LOWER("customers"."email") = LOWER('[email protected]') AND "customers"."id" != 1) LIMIT 1
(...blabla)
Started GET "/customers/signin" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by Customers::SessionsController#new as HTML
Rendered customers/sessions/new.html.erb within layouts/lightbox (40.0ms)
Rendered layouts/_metas.html.erb (0.4ms)
Rendered layouts/_messages.html.erb (0.7ms)
Rendered layouts/_footer.html.erb (1.2ms)
Completed 200 OK in 77ms (Views: 51.5ms | ActiveRecord: 0.0ms)
Started POST "/customers/signin" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by Customers::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "customer"=>{"email"=>"[email protected]", "password"=>"[FILTERED]"}, "commit"=>"Log In"}
Customer Load (4.0ms) SELECT "customers".* FROM "customers" WHERE "customers"."email" = '[email protected]' ORDER BY "customers"."id" ASC LIMIT 1
SQL (1.0ms) UPDATE "customers" SET "remember_created_at" = $1, "updated_at" = $2 WHERE "customers"."id" = 1 [["remember_created_at", 2014-05-28 16:37:05 UTC], ["updated_at", 2014-05-28 18:37:05 +0200]]
SQL (1.2ms) UPDATE "customers" SET "last_sign_in_at" = $1, "current_sign_in_at" = $2, "last_sign_in_ip" = $3, "current_sign_in_ip" = $4, "sign_in_count" = $5, "updated_at" = $6 WHERE "customers"."id" = 1 [["last_sign_in_at", 2014-05-28 16:37:05 UTC], ["current_sign_in_at", 2014-05-28 16:37:05 UTC], ["last_sign_in_ip", "127.0.0.1"], ["current_sign_in_ip", "127.0.0.1"], ["sign_in_count", 1], ["updated_at", 2014-05-28 18:37:05 +0200]]
**Redirected to http://www.example.com/customer_interface_homepage**
Completed 302 Found in 33ms (ActiveRecord: 6.2ms)
Started GET "/customer_interface_homepage" for 127.0.0.1 at 2014-05-28 18:37:05 +0200
Processing by ClientreportingPagesController#index as HTML
Customer Load (0.5ms) SELECT "customers".* FROM "customers" WHERE "customers"."id" = 1 ORDER BY "customers"."id" ASC LIMIT 1
(1.2ms) SELECT COUNT(*) FROM "roles" INNER JOIN "customers_roles" ON "roles"."id" = "customers_roles"."role_id" WHERE "customers_roles"."customer_id" = $1 AND (((roles.name = 'prospect') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL))) [["customer_id", 1]]
Rendered layouts/_metas.html.erb (0.2ms)
(0.8ms) SELECT COUNT(*) FROM "roles" INNER JOIN "customers_roles" ON "roles"."id" = "customers_roles"."role_id" WHERE "customers_roles"."customer_id" = $1 AND (((roles.name = 'superadmin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL))) [["customer_id", 1]]
Rendered layouts/client_interface_partials
Completed 200 OK in 34ms (Views: 27.7ms | ActiveRecord: 2.4ms)
Processing by DealsController#deal_page as HTML
Parameters: {"id"=>"2"}
**Completed 401 Unauthorized in 1ms**
Rendered text template (0.1ms)
(0.5ms) ROLLBACK TO SAVEPOINT active_record_2
(0.3ms) ROLLBACK TO SAVEPOINT active_record_1
(0.3ms) ROLLBACK
I'm not sure it's the root cause of the issue but 2 things seem strange to me in this log:
why does rspec send to example.com/customer_interface_homepage (i have in my spec_helper file told rspec that i test locally: Capybara.asset_host = 'http:// localhost:3000') ?
why does rspec experience a "Completed 401 Unauthorized in 1ms at the end ?
Some files that might be useful to solve the issue:
/app/models/customer_ability.rb
class CustomerAbility
include CanCan::Ability
def initialize(customer)
alias_action :show, :to => :read #this will have no change on the alias :read!
customer ||= Customer.new # guest customer (not logged in)
if customer.has_role? :superadmin
Log.info "Ability: customer is superadmin"
can :manage, :all
else
can :read, Deal do |deal|
# Only customers who have been granted access in Active Admin to a deal can read
deal.customers.include? customer
end
end
end
end
controllers/deals_controller.rb
class DealsController < ApplicationController
before_filter :authenticate_customer!,
:only => [ :deal_page ]
def deal_page
@deal = Deal.find(params[:id])
authorize! :read, @deal # only allow customers with authorized access in AA; sends to customer_ability
respond_to do |format|
format.html
format.json { render json: @deal }
end
end
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
# handle Cancan authorization exception
rescue_from CanCan::AccessDenied do |exception|
exception.default_message = t("errors.application_controller_exception_messages.only_open_to_admin")
if current_user # if it's user redirect to main HP
redirect_to root_path, :alert => exception.message
else # if it's a Customer redirect him to client interface HP
redirect_to customer_interface_homepage_path, :alert=> exception.message
end
end
def current_ability #inspired by http://mikepackdev.com/blog_posts/12-managing-devise-s-current-user-current-admin-and-current-troll-with-cancan
@current_ability ||= case
when current_user
UserAbility.new(current_user)
when current_customer
CustomerAbility.new(current_customer)
end
end
/spec/support/utilities.rb
include ApplicationHelper
def sign_in_customer(customer)
customer.confirm!
visit new_customer_session_path
fill_in "Email", with: customer.email
fill_in "Password", with: customer.password
click_on "Log In"
#populate cookie when not using capybara
cookies[:authentication_token] = customer.authentication_token
end
/spec/factories/deals.rb
FactoryGirl.define do
factory :deal do
# id i don't here any id
sequence(:deal_campaign_code) { |n| "CHA#{n}FR001" }
featured true
admin_user_id 1
end
end
/spec/factories/customers.rb
FactoryGirl.define do
factory :customer do # we use prospect as by definition a visitor signing in gets 'prospect status'
sequence(:email) { |n| "person_#{n}@example.com"}
password "bet(8a3#"
password_confirmation "bet(8a3#"
# required if the Devise Confirmable module is used
confirmed_at Time.now
confirmation_token nil
# create deals connected to the Customer
after(:create) do |customer|
customer.deals << FactoryGirl.create(:deal)
end
end
end
/app/models/customer.rb
class Customer < ActiveRecord::Base
rolify
# -- Relationships --------------------------------------------------------
has_many :customer_deals, dependent: :destroy
has_many :deals, through: :customer_deals
/app/models/deal.rb
class Deal < ActiveRecord::Base
# -- Relationships --------------------------------------------------------
belongs_to :admin_user, :foreign_key => 'admin_user_id'
has_many :customer_deals, dependent: :destroy
has_many :customers, through: :customer_deals
/app/models/customer_deal.rb
class CustomerDeal < ActiveRecord::Base
# -- Relationships --------------------------------------------------------
belongs_to :customer, :foreign_key => 'customer_id'
belongs_to :deal, :foreign_key => 'deal_id'
Upvotes: 3
Views: 1399
Reputation: 183
I think the session information is not being passed into the request you make in the spec. See this guide for how to use devise with controller tests.
As an alternative approach, I would recommend making this a feature spec instead of a controller spec. Note to make capybara play nice with devise follow this guide.
Upvotes: 1
Reputation: 83
To answer your log questions:
The spec is almost certainly failing due to current_path not being updated because of the 401 return from the deals controller. I believe if you fix that issue the current_path issue will also be fixed.
I have had problems in the past with FactoryGirl not actually writing models to the database on creation. This will cause anything relying on associations (which authorize! looks like it probably relies on due to CustomerAbility) to fail.
My suggestions moving forward are to debug authorize! (likely using log/puts statements) and figuring out what @deal actually looks like at that point as well as what deal.customers looks like. If you post the code to authorize! we may be able to help more.
Upvotes: 0