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
Related
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!
I run my test but the sign_in don't authenticate.
require 'rails_helper'
describe "get all contacts route", :type => :request do
let!(:contacts) {FactoryBot.create_list(:random_contacts, 20)}
before(:each) do
get '/api/v1/contacts'
user = FactoryBot.create(:user)
request.env["devise.mapping"] = Devise.mappings[:user]
request.headers['X-User-Email'] = "#{user.email}"
request.headers['X-User-Token'] = "#{user.authentication_token}"
sign_in user
end
it 'returns all contacts' do
expect(JSON.parse(response.body).size).to eq(20)
end
it 'returns status code 200' do
expect(response).to have_http_status(:success)
end
end
The return of the test is:
"{\"error\":\"You need to sign in or sign up before continuing.\"}"
https://github.com/andbri321/one_bit_contacts/tree/feature/test_contact
You might want to create some sign in/out helpers. Something along the lines of
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
And then load them in your RSpec config like so:
RSpec.configure do |config|
config.include DeviseRequestSpecHelpers, type: :request
end
Then use these helpers before the tests are run.
Look here for more info.
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
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