Search code examples
ruby-on-railsrspecdevisecancancancancan

unable to use sign_in method in acceptance test Rspec


I recently added Devise and CanCanCan for authentication and permission in my rails project.

As a result it broke most of my acceptance tests I made previously , for example:

resource 'Projects' do
   route '/projects?{}', 'Projects collection' do
     get 'Return all projects' do
       example_request 'list all projects' do
         expect(status).to eq 200

Why this code is broken I do know : I have no @current_user, and CanCan rejected the request with CanCan::AccessDenied.

I am currently trying to authenticate an admin user, so that my acceptance test will pass, as I defined can :manage, :all for admin.

I stumbled across many posts like mine, but no solution worked, as all of answers I found were designed for controller testing, and sign_in method apparently worked for them.

What I tried so far:

before(:each) do
   sign_in admin
end

NoMethodError:
   undefined method `sign_in' for #<RSpec::ExampleGroups::Projects::ProjectsProjectsCollection::GETReturnAllProjects:0x0000000005dc9948>

So I tried to add

RSpec.configure do |config|
 config.include Devise::TestHelpers


Failure/Error: @request.env['action_controller.instance'] = @controller

 NoMethodError:
   undefined method `env' for nil:NilClass

From what I understand I cannot do this because I am not testing in a controller scope, but I am testing a resource, so I have no @request neither @controller.

What am I doing wrong, and if not how can I make my test pass now that I included authentication & permission ?

versions used:

cancancan (2.2.0)
devise (4.3.0)
rails (5.1.4)
ruby 2.5.0p0
rspec (3.7.0)

Solution

  • The problem was exactly as described, I did not succeed in using Devise helpers in acceptance test.

    Workaround for me was to adapt from acceptance test to request test.

    # spec/requests/projects_spec.rb

    require 'rails_helper'
    
    RSpec.describe Project, type: :request do
     let!(:admin) { create(:user, is_admin: true) }
     let!(:user) { create(:user) }
    
     context 'admin logged in' do
       before do
         log_in admin
       end
       it 'Return all projects' do
         get projects_path
         expect(status).to eq 200
         // more tests
       end
       // more tests
     end
     context 'Normal user logged in' do
       before do
         log_in user
       end
       // more tests
     end
    

    The log_in method is from my own helper I created

    # spec/support/session_helpers.rb

    module SessionHelpers
      def log_in(user, valid = true, strategy = :auth0)
        valid ? mock_valid_auth_hash(user) : mock_invalid_auth_hash
        Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[strategy.to_sym]
        get user_google_oauth2_omniauth_callback_path
      end
    end
    

    It simply stub the authentication at a request level (note the get user_google_oauth2_omniauth_callback_path which is my app's authentication callback)

    My callback is configured as such :

    # app/config/routes.rb

    Rails.application.routes.draw do
      devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
      devise_scope :user do
        get 'sign_in', to: 'devise/sessions#new', as: :new_user_session
        delete 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session
      end
    

    # app/controllers/users/omniauth_callbacks_controller.rb

    module Users
      class OmniauthCallbacksController < Devise::OmniauthCallbacksController
        include Devise::Controllers::Rememberable
    
        def google_oauth2
          @user = User.from_omniauth(request.env['omniauth.auth'])
          if @user
            sign_in_and_redirect @user
          else
            redirect_to root_path, notice: 'User not registered'
          end
        end
       // more code
      end
    end
    

    Along with this other helper (my provider was Omniauth)

    # spec/support/omniauth_macros.rb

    module OmniauthMacros
      def mock_valid_auth_hash(user)
        # The mock_auth configuration allows you to set per-provider (or default)
        # authentication hashes to return during integration testing.
        OmniAuth.config.test_mode = true
        opts = {
          "provider": user.provider,
          "uid": user.uid,
          "info": {
            "email": user.email,
            "first_name": user.first_name,
            "last_name": user.last_name
          },
          "credentials": {
            "token": 'XKLjnkKJj7hkHKJkk',
            "expires": true,
            "id_token": 'eyJ0eXAiOiJK1VveHkwaTFBNXdTek41dXAiL.Wz8bwniRJLQ4Fqx_omnGDCX1vrhHjzw',
            "token_type": 'Bearer'
          }
        }
        OmniAuth.config.mock_auth[:default] = OmniAuth::AuthHash.new(opts)
      end
    
      def mock_invalid_auth_hash
        OmniAuth.config.mock_auth[:default] = :invalid_credentials
      end
    end
    

    And I required my modules so I can use them in my request tests.

    # spec/rails_helper.rb

    Dir[Rails.root.join('spec', 'support', '*.rb')].each { |file| require file }