unable to use sign_in method in acceptance test Rspec - ruby-on-rails

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)

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 }

Related

Using MiniTest, when trying to test `omniauth-google-oauth2` gem I keep getting a 302 redirect to my sign_in path

I've setup omniauth-google-oauth2 gem with my rails application. And it works but while I try to write my test for it I keep getting a 302 redirect. Many of the website results I've seen reference how to test it in Rspec, but I'm trying to do it with MiniTest that comes with Rails. Since I'm new to it I'm struggling seeing where I'm messing things up.
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
get 'sign_out', :to => 'devise/sessions#destroy', :as => :destroy_user_session
end
root to: "home#index"
end
test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
require 'mocha/minitest'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
def setup_omniauth_mock(user)
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new({
provider: "google_oauth2",
email: "#{user.email}",
first_name: "#{user.first_name}",
last_name: "#{user.last_name}"
})
Rails.application.env_config["devise.mapping"] = Devise.mappings[:user]
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
end
def login_with_user(user)
setup_omniauth_mock(user)
get user_google_oauth2_omniauth_authorize_path
end
end
module ActionController
class TestCase
include Devise::Test::ControllerHelpers
end
end
module ActionDispatch
class IntegrationTest
include Devise::Test::IntegrationHelpers
end
end
test/integration/login_with_google_oauth2_test.rb
require 'test_helper'
class LoginWithGoogleOauth2Test < ActionDispatch::IntegrationTest
setup do
#user = users(:one)
end
test "allows a logged-in user to view the home page" do
login_with_user(#user)
get root_path
assert_redirected_to root_path
assert_selector "h1", text: "#{user.full_name}"
end
end
Error when running rails test
daveomcd#mcdonald-PC9020:~/rails_projects/haystack_scout$ rails test
Running via Spring preloader in process 24855
/home/daveomcd/.rvm/gems/ruby-2.5.1/gems/spring-2.0.2/lib/spring/application.rb:185: warning: Insecure world writable dir /home/daveomcd/.rvm/gems/ruby-2.5.1/bin in PATH, mode 040777
Run options: --seed 55919
# Running:
.....F
Failure:
LoginWithGoogleOauth2Test#test_allows_a_logged-in_user_to_view_the_home_page [/mnt/c/Users/mcdonaldd/Documents/Rails Projects/haystack_scout/test/integration/login_with_google_oauth2_test.rb:20]:
Expected response to be a redirect to <http://www.example.com/> but was a redirect to <http://www.example.com/sign_in>.
Expected "http://www.example.com/" to be === "http://www.example.com/sign_in".
bin/rails test test/integration/login_with_google_oauth2_test.rb:17
Finished in 0.238680s, 25.1383 runs/s, 50.2766 assertions/s.
6 runs, 12 assertions, 1 failures, 0 errors, 0 skips
So I've been able to figure out a means to testing omniauth-google-oauth2. I'll do my best to show it below. Thanks to anyone who took the time to look into my question.
test\integration\authorization_integration_test.rb
require 'test_helper'
class AuthorizationIntegrationTest < ActionDispatch::IntegrationTest
include Capybara::DSL
include Capybara::Minitest::Assertions
include Devise::Test::IntegrationHelpers
setup do
OmniAuth.config.test_mode = true
Rails.application.env_config["devise.mapping"] = Devise.mappings[:user]
Rails.application.env_config["omniauth.auth"] = google_oauth2_mock
end
teardown do
OmniAuth.config.test_mode = false
end
test "authorizes and sets user currently in database with Google OAuth" do
visit root_path
assert page.has_content? "Sign in with Google"
click_link "Sign in with Google"
assert page.has_content? "Successfully authenticated from Google account."
end
private
def google_oauth2_mock
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new({
provider: "google_oauth2",
uid: "12345678910",
info: {
email: "fakeemail#gmail-fake.com",
first_name: "David",
last_name: "McDonald"
},
credentials: {
token: "abcdefgh12345",
refresh_token: "12345abcdefgh",
expires_at: DateTime.now
}
})
end
end

Testing devise custom session controller with rspec

I'm having custom controller
class Users::SessionsController < Devise::SessionsController
# POST /resource/sign_in
def create
binding.pry
super
end
end
routes added
devise_for :users, controllers: { sessions: "users/sessions" }
and it works during signin using browser. But inside controller test breakpoint inside create is not being hit:
RSpec.describe Users::SessionsController, type: :controller do
describe 'POST #create' do
context 'pending activation user with expired password' do
it 'could not login' do
user = create :operator_user, status: User.statuses[:activation_pending], password_changed_at: (1.day + 1.second).ago
#request.env['devise.mapping'] = Devise.mappings[:user]
sign_in user
user.reload
expect(user).to be_locked
end
end
end
end
RSpec.configure do |config|
#...
# Devise methods
config.include Devise::TestHelpers, type: :controller
# ...
end
I expect expression
sign_in user
to fall into create method that I've overrided. What am I doing wrong?
ps: it even falls into standard devise SessionsController#create
You have to send request to controller by using post :create, params: {...} inside your example instead of sign_in user

Can't sign in when running controller tests with Devise / Rails 4 / Minitest

I recently installed Devise in my app to handle auth, replacing the auth system from Michael Hartl's tutorial. It's working fine in the app itself, but I can't get my tests to auth properly, so they're pretty much all failing, which makes me sad. I'm using Rails 4 with Minitest.
Here's an example of one of my controller tests that fails:
learning_resources_controller_test.rb
require 'test_helper'
class LearningResourcesControllerTest < ActionController::TestCase
def setup
#user = users(:testuser1)
end
test "user can submit new resource" do
sign_in #user # Devise helper
post :create, {:learning_resource => {:name => "My resource"}}
resource = assigns(:learning_resource)
assert_redirected_to topic_path(#topic1, :learning_resource_created => "true")
end
end
test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
fixtures :all
# Return true is test user is signed in
def is_signed_in?
!session[:user_id].nil?
end
def sign_in_as(user, options = {})
password = options[:password] || 'password'
remember_me = options[:remember_me] || '1'
if integration_test?
# Sign in by posting to the sessions path
post signin_path, session: { email: user.email,
password: password,
remember_me: remember_me }
else
# Sign in using the session
session[:user_id] = user.id
end
end
private
def integration_test?
defined?(post_via_redirect)
end
end
class ActionController::TestCase
include Devise::TestHelpers
end
fixtures/users.yml
testuser1:
name: Test User 1
email: testuser1#mydumbdomain.com
password_digest: <%= User.digest('password') %>
The assert_redirected_to in the test always fails as the redirect is the sign in page instead of the topic page. All of my other tests fail in similar ways, indicating the user isn't signed in. I have scoured the Devise wiki and docs, but most of them cover testing with Rspec, not Minitest.
I tried using byebug within the test after the sign_in to check out the session, and I get this:
(byebug) session.inspect
{"warden.user.user.key"=>[[336453508], ""]}
If I try to call the :create, I get this error:
DEPRECATION WARNING: ActionDispatch::Response#to_ary no longer
performs implicit conversion to an array. Please use response.to_a
instead, or a splat like status, headers, body = *response. (called
from puts at
/Users/me/.rbenv/versions/2.2.2/lib/ruby/2.2.0/forwardable.rb:183)
302 {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1;
mode=block", "X-Content-Type-Options"=>"nosniff",
"Location"=>"http://test.host/signup",
"Set-Cookie"=>"request_method=POST; path=/",
"Content-Type"=>"text/html; charset=utf-8"}
Any ideas what I'm missing here?
The error is with hash
post :create, {:learning_resource => {:name => "My resource"}}
Try
post :create, :learning_resource => {:name => "My resource"}

rspec + Devise: current_user is nil in tests

I am using Devise for my user logins and stuff and rspec for testing. I have looked at the Devise testing guide for rspec and mixined ControllerMicros to controller specs.
And actually things are all working fine if I have tests organized like this:
describe 'GET #index' do
context 'user logged in but not admin' do
login_user
it 'should redirect to root_path for non_user' do
get :index
// I have asserted that the current_user here is not nil
expect(response).to redirect_to(root_path)
end
end
end
However, if I have 2 tests in the context and I got current_user is nil for the non-first test.
describe 'GET #index' do
context 'user logged in but not admin' do
login_user
it 'should redirect to root_path for non_user' do
get :index
// I have asserted that the current_user here is not nil
expect(response).to redirect_to(root_path)
end
it 'should do some other thing' do
get :index
// the current_user method returns nil here
expect(response).to redirect_to(root_path)
end
end
end
And the worst part is that it seems this problem is not deterministic: happens somewhat randomly--cause after several failed runs the suite just passed on my computer(but still fails on Travis my build)
Some additional information:
the ControllerMacro.rb
module ControllerMacros
def login_admin
before(:each) do
# #request.env["devise.mapping"] = Devise.mappings[:user]
user = User.find_by(email: 'default_admin#controller.spec')
user ||= FactoryGirl.create(:user, email: 'default_admin#controller.spec', uid: 'default_admin.controller.spec')
admin = Admin.find_by(user_id: user.id)
FactoryGirl.create(:admin, user: user) if not admin
sign_in user
end
end
def login_user(user = nil)
before(:each) do
# #request.env["devise.mapping"] = Devise.mappings[:user]
user ||= User.find_by(email: 'default_user#controller.spec')
user ||= FactoryGirl.create(:user, email: 'default_user#controller.spec', uid: 'default_user.controller.spec')
sign_in user
end
end
end
the rails_helper.rb
RSpec.configure do |config|
# for loading devise in test
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
Your login_user method is run when the test suite load, you should put it in a before :each block to run it once for each test.
describe "GET index" do
before do
login_user
end
it 'blabla' do
get :index
expect(response).to redirect_to(root_path)
end
end
PS : Don't know what you do in your login_user method, but Devise have some nice helpers you can include as follow
#rails_helper.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, type: :controller
end
#then in you test
before do
sign_in user_instance
end
UPDATE from comment
If you have multiple type of user / devise login entry, maybe try to specify the devise mapping you're trying to sign in the user to , as follow :
sign_in :user, user_instance
sign_in :admin, admin_user_instance

Rspec: Test redirects in Devise::OmniauthCallbacksController subclass

Following the Railscast on Devise and OmniAuth I have implemented an OmniauthCallbacksController < Devise::OmniauthCallbacksController which contains a single method to handle an OmniAuth callback:
def all
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
alias_method :facebook, :all
routes.rb:
devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks", :sessions => "sessions" }
I would like to customise this, so I'm trying to test it using RSpec. The question is how do I test this method and the redirects?
If in the spec I put user_omniauth_callback_path(:facebook) it doesn't complain about the route not existing, but doesn't seem to actually call the method.
According to this answer "controller tests use the four HTTP verbs (GET, POST, PUT, DELETE), regardless of whether your controller is RESTful." I tried get user_... etc. but here it does complain that the route doesn't exist. And indeed if I do rake routes it shows there is no HTTP verb for this route:
user_omniauth_callback [BLANK] /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:facebook)
Can you see what I'm missing?
EDIT
So following this question one way of calling the method is:
controller.send(:all)
However I then run into the same error that the questioner ran into:
ActionController::RackDelegation#content_type delegated to #_response.content_type, but #_response is nil
You will need to do three things to get this accomplished.
enter OmniAuth test environment
create an OmniAuth test mock
stub out your from_omniauth method to return a user
Here is a possible solution, entered in the spec itself
(spec/feature/login_spec.rb for example) . . .
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new({
provider: :facebook,
uid:'12345',
info: {
name: "Joe"
}
})
User.stub(:from_omniauth).and_return(current_user)
end
I adapted this from a google authentication, so facebook may require more fields, but those are the only ones required by omniauth docs. You should be able to find the correct fields by looking at your database schema and finding fields that match the documentation.
In my case, the minimum was enough to pass the request phase and move onto the stubbed out method returning my user.
This example also uses FactoryGirl.
It may not be perfect, but I hope it helps. Good luck!
-Dan
If you hit this and you are running rspec 3.4 this example should work for you:
describe Users::OmniauthCallbacksController, type: :controller do
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:your_oauth_provider_here] = OmniAuth::AuthHash.new(
provider: :your_oauth_provider_here,
uid: rand(5**10),
credentials: { token: ENV['CLIENT_ID'], secret: ENV['CLIENT_SECRET'] }
)
request.env['devise.mapping'] = Devise.mappings[:user]
allow(#controller).to receive(:env) { { 'omniauth.auth' => OmniAuth.config.mock_auth[:your_oauth_provider_here] } }
allow(User).to receive(:from_omniauth) { current_user }
end
describe '#your_oauth_provider_here' do
context 'new user' do
before { get :your_oauth_provider_here }
it 'authenticate user' do
expect(warden.authenticated?(:user)).to be_truthy
end
it 'set current_user' do
expect(current_user).not_to be_nil
end
it 'redirect to root_path' do
expect(response).to redirect_to(root_path)
end
end
end
end
I am experiencing problem for writhing RSpec for OmniauthCallbacksController, do some research on this and it working for me. Here is my codes, if anyone found necessary. Tests are for happy path and it should work for news version of RSpec eg. 3.x
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
describe "#linkedin" do
let(:current_user) { Fabricate(:user) }
before(:each) do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:linkedin] = OmniAuth::AuthHash.new({provider: :linkedin, uid: '12345', credentials: {token: 'linkedin-token', secret: 'linkedin-secret'}})
request.env["devise.mapping"] = Devise.mappings[:user]
#controller.stub!(:env).and_return({"omniauth.auth" => OmniAuth.config.mock_auth[:linkedin]})
User.stub(:from_auth).and_return(current_user)
end
describe "#linkedin" do
context "with a new linkedin user" do
before { get :linkedin }
it "authenticate user" do
expect(warden.authenticated?(:user)).to be_truthy
end
it "set current_user" do
expect(subject.current_user).not_to be_nil
end
it "redirect to root_path" do
expect(response).to redirect_to(root_path)
end
end
end
end
end

Resources