0
votes

I'm working through a Rails course trying to function test my app. I'm running into an issue that seems to indicate that Rspec isn't using the helper needed for it to work with Devise (CanCanCan).

Link to repo below. I'm a fairly new developer but make an effort for it to be readable. Thanks, all feedback appreciated.

The issue comes from my users_controller_spec.rb file:

Failures:

  1) UsersController GET #show User is logged in loads correct user details
 Failure/Error: if user&.admin?

 NoMethodError:
   undefined method `admin?' for #<User:0x00000005c08eb0>
 # ./app/models/ability.rb:14:in `initialize'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_additions.rb:361:in `new'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_additions.rb:361:in `current_ability'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_additions.rb:342:in `authorize!'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_resource.rb:49:in `authorize_resource'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_resource.rb:34:in `load_and_authorize_resource'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/cancancan-1.15.0/lib/cancan/controller_resource.rb:10:in `block in add_before_action'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/rails-controller-testing-1.0.1/lib/rails/controller/testing/template_assertions.rb:61:in `process'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/devise-4.2.0/lib/devise/test/controller_helpers.rb:33:in `block in process'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/devise-4.2.0/lib/devise/test/controller_helpers.rb:100:in `catch'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/devise-4.2.0/lib/devise/test/controller_helpers.rb:100:in `_catch_warden'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/devise-4.2.0/lib/devise/test/controller_helpers.rb:33:in `process'
 # /home/scorpian55/.rvm/gems/ruby-2.3.1@unit4-1/gems/rails-controller-testing-1.0.1/lib/rails/controller/testing/integration.rb:12:in `block (2 levels) in <module:Integration>'
 # ./spec/controllers/users_controller_spec.rb:15:in `block (4 levels) in <top (required)>'

Finished in 0.36791 seconds (files took 4.43 seconds to load)
7 examples, 1 failure

Failed examples:

rspec ./spec/controllers/users_controller_spec.rb:14 # UsersController GET #show User is logged in loads correct user details

In researching, most errors I've seen have been around the NoMethodError for nil:NilClass and I am not getting that same error, that is a different issue and not the exact same error message.

I've checked:

1) spec/rails_helper.rb has:

config.include Devise::Test::ControllerHelpers, :type => :controller

2) spec/spec_helper.rb has:

require 'spec_helper'
require 'rspec/rails'
# note: require 'devise' after require 'rspec/rails'
require 'devise'

RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, :type => :controller
end

per: https://github.com/plataformatec/devise/wiki/How-To:-Test-controllers-with-Rails-3-and-4-(and-RSpec)#controller-specs

Here are the relevant files (and my repo: https://github.com/ScorpIan555/gitwork):

users_controller_spec.rb

 require 'rails_helper'

describe UsersController, :type => :controller do

let(:user) { User.create!(email: "user#{rand(100000).to_s}@examples.com", password: '1234567890') }

  describe 'GET #show' do

 context 'User is logged in' do
  before do
    sign_in user
  end

  it 'loads correct user details' do
    get :show, id: user.id
    expect(response).to have_http_status(200)
    expect(assigns(:user)).to eq user
  end

end #end of first context

 context 'No user is logged in' do
   it 'redirects to login' do
     get :show, id: user.id
     expect(response).to redirect_to(new_user_session_path)
   end
 end

  end #end of GET#show block

end #end of whole block

ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)

# Define abilities for the passed in user here. For example:
#
 user ||= User.new # guest user (not logged in)

 alias_action :create, :read, :update, :destroy, to: :crud

 can :manage, User, id: user.id

  if user&.admin?

    can :crud, Product
    can :crud, User
    can :crud, Comment

   elsif user&.signed_in?

    can :read, Comment
    can :create, Comment
    can :read, Product
    #can :invite, :User

  else
    can :read, Comment
    can :read, Product

   end #end if/else
  end #end def initialize(user)
end  #end class Ability

users_controller.rb:

  class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!
  load_and_authorize_resource

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit
  end

  #  POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was     successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
    format.json { render json: @user.errors, status: :unprocessable_entity }
  end
end
  end

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
respond_to do |format|
  if @user.update(user_params)
    format.html { redirect_to @user, notice: 'User was successfully updated.' }
    format.json { render :show, status: :ok, location: @user }
  else
    format.html { render :edit }
    format.json { render json: @user.errors, status: :unprocessable_entity }
  end
end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
def set_user
  @user = User.find(params[:id])
end

# Never trust parameters from the scary internet, only allow the white list through.
def user_params
  params.require(:user).permit(:first_name, :last_name)
end
end

schema.db

ActiveRecord::Schema.define(version: 20161219012011) do

  create_table "comments", force: :cascade do |t|
t.integer  "user_id"
t.text     "body"
t.integer  "rating"
t.integer  "product_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["product_id"], name: "index_comments_on_product_id"
t.index ["user_id"], name: "index_comments_on_user_id"
  end

  create_table "orders", force: :cascade do |t|
t.integer "user_id"
t.integer "product_id"
t.float   "total"
t.index ["product_id"], name: "index_orders_on_product_id"
t.index ["user_id"], name: "index_orders_on_user_id"
  end

  create_table "products", force: :cascade do |t|
t.string   "name"
t.text     "description"
t.string   "image_url"
t.datetime "created_at",  null: false
t.datetime "updated_at",  null: false
t.string   "color"
t.decimal  "price"
  end

  create_table "users", force: :cascade do |t|
t.string   "first_name"
t.string   "last_name"
t.datetime "created_at",                          null: false
t.datetime "updated_at",                          null: false
t.string   "email",                  default: "", null: false
t.string   "encrypted_password",     default: "", null: false
t.string   "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer  "sign_in_count",          default: 0,  null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string   "current_sign_in_ip"
t.string   "last_sign_in_ip"
t.boolean  "admin",                  default: false, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :orders
  has_many :comments  #added troubleshooting 6.3, need to test

end
1
Can you post the definition of load_and_authorize_resource?31piy
Sorry, where would I find that? I thought that's a helper defined by Devise that I was able to use after correctly installing the Gem.ScorpIan
This is what I found on Ryan Bates' github: github.com/ryanb/cancan/wiki/authorizing-controller-actionsScorpIan
Can you post schema definition of User model?31piy
I'm not well versed with CanCanCan, but I suppose, somehow your ability class and User is not connected with each other. That's why the ability class is not able to call user's admin? method.31piy

1 Answers

0
votes

There was nothing obvious wrong with your code so I retrieved your repo and after installing the gems and running the db:setup I found that I replicated your error.

I am not sure where that schema is from that you posted in your question but it's not the one your database is using. The one in the repo does not have the admin boolean in it:

  create_table "users", force: :cascade do |t|
    t.string   "first_name"
    t.string   "last_name"
    t.datetime "created_at",                             null: false
    t.datetime "updated_at",                             null: false
    t.string   "email",                  default: "",    null: false
    t.string   "encrypted_password",     default: "",    null: false
    t.string   "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer  "sign_in_count",          default: 0,     null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string   "current_sign_in_ip"
    t.string   "last_sign_in_ip"
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

I do note that you do have a migration to add that column though. Somehow your migration and schemas have got out of sync, (possibly you reverted back to previous schema after running the migration). The best thing to do is to recreate your migration that adds that column:

rails g migration add_admin_flag_to_users --force

edit the migration and add the following:

add_column :users, :admin, :boolean, default: false, null: false

run rails db:migrate and rails db:migrate RAILS_ENV=test

the following now works:

$ rails c
Running via Spring preloader in process 60107
Loading development environment (Rails 5.0.0.1)
2.3.1 :001 > user = User.new
 => #<User id: nil, first_name: nil, last_name: nil, created_at: nil, updated_at: nil, email: "", admin: false>
2.3.1 :002 > user.admin?
 => false
2.3.1 :003 >

Note your tests will still fail on validations, but that's a completely separate issue and one that you can investigate now that you have passed this hurdle :)

Incidentally, in your research where you encountered NoMethodError for nil classes, your code is protected against that:

user&.admin?

The & character is basically saying only try this if user is not nil, equivalent to:

user.admin? if user