I'm trying to do a simple test to make sure that when a user logs in it redirects them to the homepage with a 'successful sign in' message. However, when I run the test I keep getting the following response. I have attempted to change the after_sign_in_path_for(resource) to the root_url in application controller with no luck.
Failures:
1) Users signs in a user
Failure/Error: expect(response).to redirect_to(root_url)
Expected response to be a <3XX: redirect>, but was a <200: OK>
# ./spec/requests/user_spec.rb:8:in `block (2 levels) in <main>'
Finished in 0.42947 seconds (files took 3.97 seconds to load)
6 examples, 1 failure
Failed examples:
rspec ./spec/requests/user_spec.rb:4 # Users signs in a user
Here is the test
user_spec.rb
require 'rails_helper'
RSpec.describe 'Users', type: :request do
it 'signs in a user' do
create(:user, email: 'test#test.com', password: 'password')
get '/users/sign_in'
post '/users/sign_in', params: { email: 'test#test.com', password: 'password' }
expect(response).to redirect_to(root_url)
expect(flash[:notice]).to eq('Signed in successfully.')
end
end
rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
require 'support/factory_bot'
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers, type: :request
end
Application Controller
class ApplicationController < ActionController::Base
include Pundit::Authorization
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :authenticate_user!, except: :index
before_action :set_render_cart
before_action :initialize_cart
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = 'You are not authorized to perform this action.'
redirect_to(request.referrer || root_path)
end
def set_render_cart
#render_cart = true
end
def initialize_cart
#cart ||= Cart.find_by(id: session[:cart_id])
if #cart.nil?
#cart = Cart.create
session[:cart_id] = #cart.id
end
end
Any help would be greatly appreciated!
Related
I have a controller concern defined as follows:
module Auth
extend ActiveSupport::Concern
private
def sign_in(user)
session[:customer_id] = user.id
self.current_user = user
end
def signed_in?
!current_user.nil?
end
...
# other methods
end
And this module is included in the ApplicationController:
class ApplicationController < ActionController::Base
include Auth
helper_method :signed_in?, :current_user, :current_user?
before_action :authorize
end
The questions I have:
should we really extend extend ActiveSupport::Concern in a concern? If so wy and when not?
how can I enable the concern module so that its methods could be re-used in RSpec request specs?
I tried to add the following in spec/support/utilities.rb:
include ApplicationHelper
include Auth
as well as required all the files from spec/support directory in rails_helper.rb and included the above modules:
# Require all the files in support directory
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
...
# include ApplicationHelper
config.include ApplicationHelper, type: :feature
config.include Auth, type: :request
...
end
Now, when running a requests/customers_spec.rb:
require 'rails_helper'
RSpec.describe "/customers", type: :request do
describe "GET /index" do
it "renders a successful response" do
customer = create(:customer)
sign_in(customer)
get customers_url
expect(response).to be_successful
end
end
...
it fails with:
Failures:
1) /customers GET /index renders a successful response
Failure/Error: session[:customer_id] = user.id
NoMethodError:
undefined method `session' for nil:NilClass
# ./app/controllers/concerns/auth.rb:7:in `sign_in'
# ./spec/requests/customers_spec.rb:10:in `block (3 levels) in <top (required)>'
What am I missing?
I'm using Rails 6.0.3, rspec-rails 4.0.1.
The solution I came to was to add the following method to the spec/support/login_macros.rb:
module LoginMacros
def sign_in_with_browser(user)
visit login_path
fill_in 'E-mail', with: user.email
fill_in 'Password', with: user.password
click_button 'Login'
end
def sign_out_with_browser
click_link 'Logout'
end
# to enable user authentication in request specs
def login_without_browser(user)
post login_url, params: { email: user.email, password: 'secret' }
end
end
Then test a protected end-point in the above request spec as follows:
describe "GET /index" do
it "renders a successful response" do
customer = create(:customer)
login_without_browser(customer)
get customers_url
expect(response).to be_successful
end
end
I override SessionsController in my own controller and tried to test with RSpec. Fist, I setup devise with
#request.env["devise.mapping"] = Devise.mappings[:user]
my spec:
require 'rails_helper'
require 'authentication_helper'
RSpec.describe Users::SessionsController, type: :controller do
include AuthenticationHelper
describe 'create new session' do
before(:each) do
setup_auth
end
let(:user) {FactoryGirl.create(:user, username: 'john', email: 'john#gmail.com', password: 'pwd1234')}
it 'should return 200 with valid username and password' do
post :create, user: {login: 'john', password: 'pwd1234'}
expect(response).to have_http_status 200
expect(controller.current_user.id).to eq(user.id)
end
end
end
my SessionsController just return http 401 or http 200.
When I run my spec, I get this error:
NoMethodError:
undefined method authenticate?' for nil:NilClass
# /usr/local/bundle/gems/devise-3.5.6/app/controllers/devise_controller.rb:103:inrequire_no_authentication'
# ./spec/controllers/users/sessions_controller_spec.rb:16:in block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:45:inblock (3 levels) in '
# /usr/local/bundle/gems/database_cleaner-1.6.0/lib/database_cleaner/generic/base.rb:16:in cleaning'
# /usr/local/bundle/gems/database_cleaner-1.6.0/lib/database_cleaner/base.rb:98:incleaning'
# /usr/local/bundle/gems/database_cleaner-1.6.0/lib/database_cleaner/configuration.rb:86:in block (2 levels) in cleaning'
# /usr/local/bundle/gems/database_cleaner-1.6.0/lib/database_cleaner/configuration.rb:87:incall'
# /usr/local/bundle/gems/database_cleaner-1.6.0/lib/database_cleaner/configuration.rb:87:in cleaning'
# ./spec/rails_helper.rb:44:inblock (2 levels) in '
What am I doing wrong?
You know that Devise offers RSpec test helpers for controller specs. However, in request specs, they will not work.
Here is a solution for request specs, adapted from the Devise wiki. We will simply use Warden's test helpers – you probably already load them for your Cucumber tests.
First, we define sign_in and sign_out methods. These will behave just like those you know from controller specs:
module DeviseRequestSpecHelpers
include Warden::Test::Helpers
def sign_in(resource_or_scope, resource = nil)
resource ||= resource_or_scope
scope = Devise::Mapping.find_scope!(resource_or_scope)
login_as(resource, scope: scope)
end
def sign_out(resource_or_scope)
scope = Devise::Mapping.find_scope!(resource_or_scope)
logout(scope)
end
end
Finally, load that module for request specs:
RSpec.configure do |config|
config.include DeviseRequestSpecHelpers, type: :request
end
Done. You can now write request specs like this:
sign_in create(:user, name: 'John Doe')
visit root_path
expect(page).to include('John Doe')
Reference:
https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs
You have to stub out the warden.authenticate! call, not just the current_user method.
For login success test cases:
before(:each) do
# assuming `user` is defined and returns a User instance
allow(request.env['warden']).to receive(:authenticate!).and_return(user)
allow(controller).to receive(:current_user).and_return(user)
end
For failure cases:
before(:each) do
allow(request.env['warden']).to receive(:authenticate!).and_throw(:warden, scope: :user)
allow(controller).to receive(:current_user).and_return(nil)
end
This works for me in devise 4.x. Found this in https://github.com/plataformatec/devise/wiki/How-To:-Stub-authentication-in-controller-specs
You should have a helper for the sign in, for example
module AuthenticationHelpers
module Controller
def sign_in(user)
controller.stub(:current_user).and_return(user)
controller.stub(:user_id).and_return(user.id)
end
end
module Feature
def sign_in(user, options={})
visit "/users/sign_in"
fill_in "Email", with: user.email
fill_in "Password", with: options[:password]
click_button "Log in"
end
end
end
Any concerns, do not hesitate to comment
Devise: 4.20
Rails: 5.0.1
Rspec: 3.5
I had used this link https://github.com/plataformatec/devise/wiki/How-To:-Use-HTTP-Basic-Authentication, but I havproblems to test the http basic auth for my api using rspec for requests tests. Below is the example the error:
app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
before_action :authenticate_user!
protected
def authenticate_user!
authenticate_or_request_with_http_basic do |username, password|
resource = User.find_by_username(username)
if resource
sign_in :user, resource if resource.valid_password?(password)
else
request_http_basic_authentication
end
end
end
end
end
app/controllers/api/v1/car_controller.rb
module Api
module V1
class CarController < Api::BaseController
respond_to :json
def index
#cars = Car.all
render :json => {:content => #cars}, :status => 200
end
end
end
end
spec/requests/api/v1/car_controller_spec.rb
require 'rails_helper'
RSpec.describe "Servers API", :type => :request do
it 'sends a list of servers' do
admin = FactoryGirl.create(:admin)
#env = {}
#env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(admin.username, admin.password)
get "/api/cars", :params => {}, :headers => #env
# test for the 200 status-code
expect(response.status).to eq(200)
end
end
When I run the spec, I have the below error:
# --- Caused by: ---
# NoMethodError:
# undefined method `sign_in' for #<Api::V1::CarController:0x0000000609ef12>
# ./app/controllers/api/base_controller.rb:10 in block in authenticate_user!
Anyone can help me? Thanks.
I have similar setup where my specs are passing, would you also show your spec_helper content, looks like you are not including Devise::TestHelpers.
spec_helper
RSpec.configure do |config|
config.include Devise::TestHelpers
config.include Warden::Test::Helpers
config.before { Warden.test_mode! }
config.after { Warden.test_reset! }
config.before(:each) do
#headers = { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
end
end
and my test looks something like this:
RSpec.describe 'Users' do
context 'when client is authorized' do
let(:user) { FactoryGirl.create(:user) }
it 'gets user' do
#headers['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.
encode_credentials(
user.authentication_token,
email: user.email
)
get api_v1_user_url(id: user.id), {}, #headers
expect(response.status).to eq(200)
end
end
end
I'm currently attempting my first unit test and I'm receiving the following errors
Failures:
1) StaticPagesController GET #index responds successfully with an HTTP 200 status code
Failure/Error: get :index
NoMethodError:
undefined method `authenticate!' for nil:NilClass
# /Users/danielmayle/.rvm/gems/ruby-2.2.1/gems/devise-3.5.2/lib/devise/controllers/helpers.rb:112:in `authenticate_user!'
# ./spec/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>'
2) StaticPagesController GET #index renders the index template
Failure/Error: get :index
NoMethodError:
undefined method `authenticate!' for nil:NilClass
# /Users/danielmayle/.rvm/gems/ruby-2.2.1/gems/devise-3.5.2/lib/devise/controllers/helpers.rb:112:in `authenticate_user!'
# ./spec/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>'
Here is my unit test code:
require 'rails_helper'
describe StaticPagesController, :type => :controller do
context "GET #index" do
before do
get :index
end
it "responds successfully with an HTTP 200 status code" do
expect(response).to be_success
expect(response).to have_http_status(200)
end
it "renders the index template" do
expect(response).to render_template("index")
end
end
end
And here is my static_controller.rb code:
class StaticPagesController < ApplicationController
def index
end
def landing_page
#featured_product = Product.first
end
def thank_you
#name = params[:name]
#email = params[:email]
#message = params[:message]
UserMailer.contact_form(#email, #name, #message).deliver_now
end
end
Why do are these errors coming up and how do I fix the problem? I've only been coding for a few months so any assistance would be much appreciated. Thanks :)
Update
Here is my application controller
class ApplicationController < ActionController::Base
before_action :authenticate_user!
before_action :configure_permitted_parameters, if: :devise_controller?
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
rescue_from CanCan::AccessDenied do |exception|
redirect_to main_app.root_url, :alert => exception.message
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :username
end
end
Here is my user_mailer code as well
class UserMailer < ActionMailer::Base
default from: "dmayle012#gmail.com"
def contact_form(email, name, message)
#message = message
mail(:from => email,
:to => 'dmayle012#gmail.com',
:subject => "New ActionMail Message from #{name}")
end
end
Your problem is this line:
before_action :authenticate_user!
This makes devise to check authorisation for current_user, which is nil in your test. There are two ways to fix it depending on what your requirements are.
Firstly, if you want any internet user to be able to view your static pages without login, add:
skip_before_action :authenticate_user!
in your StaticPagesController. This will tell devise that you don't require current_user to be allowed to view the page (not really - it doesn't tell devise anything, it just not asking it to authorise user).
Second option - you need user to be logged in to view those pages. In this case you need to create fake session before you start your test using some helper methods provided by devise. This is very easy and well documented process, you can find the steps here: https://github.com/plataformatec/devise/wiki/How-To:-Test-controllers-with-Rails-3-and-4-(and-RSpec)#controller-specs. Not adding those steps in the answer as I believe you will go with first case (since every page requires some pages available without the session, at least for the login page).
Devise has some helper methods specifically for this. Here is how to setup your controller spec to get past your undefined method authenticate_user! for nil error
First you need to include them in your rails_helper.rb like this
# spec/rails_helper.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, type: :controller
end
And then you can use the helper like this in your static pages controller
# spec/controllers/static_pages_controller_spec.rb
describe StaticPagesController, :type => :controller do
context "GET #index" do
before :each do
user = FactoryGirl.create(:user, approved: true)
sign_in :user, user
get :index
end
...
end
end
gemfile:
source 'https://rubygems.org'
gem 'rails', '4.1.1'
gem 'mysql2
spec/spec_helper.rb
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment",__FILE__)
require 'rspec/rails'
require "capybara/rspec"
include Capybara::DSL
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
spec/support/controller_macros.rb
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
# user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
def load_key
session[:master_key] = SecureRandom.hex(24)
end
end
spec/factories/factory.rb
FactoryGirl.define do
factory :user do
email "test#test.com"
password "12345678"
end
end
categories_controller.rb
class CategoriesController < ApplicationController
before_filter :authenticate_user!, :load_key!
def index
#categories = Category.where("user_id is null or user_id = ?", current_user).order(updated_at: :desc)
end
private
def category_params
params.require(:category).permit(:title)
end
end
spec/controllers/categories_controller_spec.rb
require 'rails_helper'
describe CategoriesController do
login_user
load_key
it "get list of categories" do
get :index
expect(response).to render_template("index")
end
end
application_controller.rb
class ApplicationController < ActionController::Base
def load_key!
redirect_to(key_path) unless session[:master_key]
end
end
I would like to bypass a load_key! before_filer in CategoriesController. I added method load_key to controller_macros.rb. But it gives me the error:
undefined local variable or method 'session' for RSpec::ExampleGroups::CategoriesController:Class (NameError) from d:/sites/key_manager/spec/support/controller_macros.rb:19:in `load_key'
Looks like session variable is unavailable under rspec.
The answer was a quite simple.
I should use request.session rather than session.
In this case I should transfer a session variable assign from controller_macros.rb to categories_controller_spec.rb:
categories_controller_spec.rb
require 'rails_helper'
require 'spec_helper'
describe CategoriesController, :type => :controller do
login_user
it "redirect to key form when key was't loaded" do
get :index
expect(response).to redirect_to(key_path)
end
it "view password list #index" do
request.session[:master_key] = SecureRandom.hex(24) # <======================
get :index
expect(response).to render_template(:index)
end
end
controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in FactoryGirl.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
# user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
end