I have a request test spec which tests a POST request.
In my PaymentController (which handles the POST request), i have
before_action :require_user, :require_order
which basically gets the user and order based on the session.
Any idea how i can set session variable(user_id and order_id) in my request test spec?
Since Rails 5.0 the best way is to use the keyword arguments in the controller/request tests:
get :index, params: { ... }, session: { user_id: 1, ... }
If you are using a authentication library like Devise, Clearance and such, there are various helpers to stub a logged in user, see here the Documentation for Devise:
before(:each) do
# or def setup if using minitest
#request.env["devise.mapping"] = Devise.mappings[:user]
sign_in User.create(...)
end
For Rails < 5, this solution works
def _set_session(key, value)
Warden.on_next_request do |proxy|
proxy.raw_session[key] = value
end
end
_set_session(:access_acount_id, user.id)
Example
def _set_session(key, value)
Warden.on_next_request do |proxy|
proxy.raw_session[key] = value
end
end
context 'When there is something' do
let(:yesterday) { 1.day.ago }
let(:updates_enabled) { true }
before do
_set_session(:access_acount_id, user.id)
login_as(user, scope: :user)
end
it 'does not update records' do
visit dashboard_path
expect(page).to have_css('.popup#my-popup')
end
end
Related
I've been trying to stub a private module method for the whole day now but with not progress.
Here is a snippet of my application controller class
class ApplicationController < ActionController::Base
include Cesid::Application
end
Cesid > Application.rb
module Cesid
module Application
extend ActiveSupport::Concern
included do
before_action :track_marketing_suite_cesid, only: [:new]
end
private
def track_marketing_suite_cesid
return unless id_token_available?
## #cesid_auth = Auth.new(#id_token)
#cesid_auth = Auth.new(id_token)
return unless #cesid_auth.present? && #cesid_auth.valid?
#cesid_admin = Admin.where(email: #cesid_auth.email).first_or_initialize
end
def id_token_available?
## #id_token.present?
id_token.present?
end
def id_token
#id_token ||= id_token_param
end
def id_token_param
cookies[:id_token]
end
end
end
Now, I'm trying to create a simple unit test for the method
id_token_available?
And I am just trying to set the id_token_param to a random value.
I've tried using this code as stated Is there a way to stub a method of an included module with Rspec?
allow_any_instance_of(Cesid).to receive(:id_token_param).and_return('hello')
but I just get this error
NoMethodError:
undefined method `allow_any_instance_of' for #<RSpec::ExampleGroups::CesidApplication::CesidAuthorizations::GetCesidApplication:0x00007fa3d200c1c0> Did you mean? allow_mass_assignment_of
Rspec file
require 'rails_helper'
describe Cesid::Application, :type => :controller do
describe 'cesid application' do
before do
allow_any_instance_of(ApplicationController).to receive(:id_token_param).and_return('hello')
end
it 'returns true if the id_token is present' do
expect(Cesid::Application.send('id_token_available?')).to eql(true)
end
end
end
Rspec version
3.5.4
This is honestly starting to drive me crazy
I see three issues:
You call allow_any_instance_of in a context in which it is not defined. allow_any_instance_of can be used in before blocks. I need to see your RSpec code to be more specific.
Actually your code is called on the ApplicationController, not on the module, therefore you need to change your stub to
allow_any_instance_of(ApplicationController).to receive(:id_token_param).and_return('hello')
Currently id_token_param will not be called at all, because id_token_available? checks the instance variable and not the return value of the id_token method that calls the id_token_param. Just change the id_token_available? to:
def id_token_available?
id_token.present?
end
There's a much better way of going about this test. The type: :controller metadata on your spec gives you an anonymous controller instance to work with.
Here's an example of how you could write this to actually test that the before_action from your module is used:
describe Cesid::Application, type: :controller do
controller(ApplicationController) do
def new
render plain: 'Hello'
end
end
describe 'cesid before_action' do
before(:each) do
routes.draw { get 'new' => 'anonymous#new' }
cookies[:id_token] = id_token
allow(Auth).to receive(:new).with(id_token)
.and_return(instance_double(Auth, valid?: false))
get :new
end
context 'when id token is available' do
let(:id_token) { 'hello' }
it 'sets #cesid_auth' do
expect(assigns(:cesid_auth)).to be_present
end
end
context 'when id token is unavailable' do
let(:id_token) { '' }
it 'does not set #cesid_auth' do
expect(assigns(:cesid_auth)).to be_nil
end
end
end
end
I understand this question has been asked before in various forms.
However I'm struggling with something that doesn't seem to be being solved in these answers. My logged in user is not persisting within the spec.
How are you supposed to replicate authentication/logged in users in a request spec?
Here's what I've tried, and what I'm doing.
I'm using Auth0 as my authentication handler. I have a signin method that's called in the Auth0 Callback, so I've jerry-rigged a mock_auth endpoint for my tests to utilize a resource object.
This is my current set up and what I've done to try and replicate the login flow.
#/spec/requests/api/v1/account_spec.rb
RSpec.describe "API V1 Accounts", type: :request do
# Factories.
...
describe "PATCH update" do
subject(:http_request) { patch endpoint, params: { account: account_params, format: :json } }
# set some defaults
let(:id) { account.id }
let(:endpoint) { "/api/v1/accounts/#{id}" }
let(:account_params) { {} }
# Configure subdomain contstraint
within_subdomain :api do
before do |example|
mock_login(resource) unless example.metadata[:skip_signin]
http_request
end
context "when no resource is logged in", :skip_signin do
# This spec passes fine, as it's skipping login.
it_behaves_like "an unauthenticated private api request"
end
context "when there is no record to be found" do
let(:id) { SecureRandom.uuid }
let(:resource) { create(:user) }
it "fails to access a record" do
expect(response).to have_http_status(:not_found)
end
end
xcontext "when the user has access permission" do
end
end
end
end
-
# config/routes.rb
post "/auth/mock/:id", to: "auth#mock", as: :mock_login if Rails.env.test?
-
# auth_controller.rb
def mock
return unless Rails.env.test?
#resource = User.find_by(params[:id]
signin(#resource)
end
def signin(resource)
reset_session
create_session(resource)
after_signin_redirect_for(resource)
end
and I'm using this helper to call it from my request spec
module Helpers
module Auth
def mock_login(resource)
post mock_login_path(resource.id)
end
end
end
RSpec.configure do |config|
config.include Helpers::Auth, type: :request
end
So. By throwing around a bunch of debuggers and binding.pry I can see that my mock_login(resource) is being called successfully and at the end of the signin method, my helper signed_in? is true. Having successfully set a session.
The issue that I'm having now, is that this is not persisting in the feature spec when it's run in the before block, or in the it block.
before do |example|
mock_login(resource) unless example.metadata[:skip_signin] # signed_in? == true!
http_request # signed_in? == nil
end
module API
module V1
class AccountsController < APIController
before_action :authenticate_resource!
# ^ This is where the spec is failing to recognise the signed in resource from the mock_login method.
before_action :set_account
# PATCH /api/v1/accounts/:id
def patch_update
# Cancancan Authorization
authorize! :update, #account
# handle patch
...
end
private
def set_account
binding.pry # We're never making it here.
#account = Account.find_by(id: params[:id])
end
...
end
end
end
def authenticate_resource!
return true if signed_in?
respond_to do |format|
format.json { head(:unauthorized) }
end
end
EDIT: A couple of changes to make it clearer what I'm asking.
I'm trying to do integration test for our login process we have overwrite session create process like this:
module UsersDashboard
class SessionsController < Devise::SessionsController
protect_from_forgery :except => [:create, :mfa_callback, :authenticate]
def test
session[:test] = 'doodoo'
self.resource = warden.authenticate!(auth_options)
render text: 'hi'
end
# Used for User Dashboard login
def create
current_user = warden.authenticate!(auth_options)
resource = current_user
current_user.update_attribute(:mfa_authenticated, false)
# some code here
session[:channel] = 'some value'
p "session channel in create: #{session[:channel]}, #{session.class}"
redirect_url = 'some value here'
redirect_to redirect_url
end
end
and here is my test code:
require 'test_helper'
include Warden::Test::Helpers
class DashboardOkTest < ActionDispatch::IntegrationTest
test "Dashboard sign in and visiting various pages inside dashboard " do
post_via_redirect '/users_dashboard/sessions/mani'
p session[:test]
mani = users(:mani)
post_via_redirect '/users/sign_in', 'user[email]' => mani.email, 'user[password]' => '123456'
assert_response :success
assert_equal '/mfa/index', path
assert_select '#choose-mfa-method', 'Choose Your Acceptto Multi-Factor Auth Method:'
login_as mani, scope: :user
p "session: #{session[:channel]}, #{session.inspect}"
assert request.query_parameters.has_key?(:channel)
tfa = Tfa.find_by_channel(request.query_parameters[:channel])
assert tfa
tfa.update_attribute(:status, 'approved')
get "/users_dashboard/sessions/authenticate?channel=#{tfa.channel}"
end
end
as you can see in the output:
"doodoo"
"session channel in create: 123, ActionDispatch::Request::Session"
"session: , #<ActionDispatch::Request::Session:0x7fabecaa9458 not yet loaded>"
"session channel is nil"
session[:channel] doesn't work and session is not loaded yet! interesting point is I have created a test method in Session Controller as you can see and if we set session[:test] before calling self.resource = warden.authenticate!(auth_options) session works and it prints 'doodoo' but it's not the case for create method if we set session[:channel] before self.resource = warden.authenticate!(auth_options) it still doesn't work.
It looks like devise has some kind of wrapper that nullifies session after create call. and all this just happens in test while session and everything else works in development server in web.
Any idea how to set a session for test inside create method?
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
Here my http basic authentication in the application controller file (application_controller.rb)
before_filter :authenticate
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "username" && password == "password"
end
end
and the default test for the index action of my home controller (spec/controllers/home_controller_spec.rb)
require 'spec_helper'
describe HomeController do
describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
end
end
Test doesn't run because of the authentication method. I could comment "before_filter :authenticate" to run them but I would like to know if there is way to make them worked with the method.
Thank you!
Update (2013): Matt Connolly has provided a GIST which also works for request and controller specs: http://gist.github.com/4158961
Another way of doing this if you have many tests to run and don't want to include it everytime (DRYer code):
Create a /spec/support/auth_helper.rb file:
module AuthHelper
def http_login
user = 'username'
pw = 'password'
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
end
end
In your test spec file:
describe HomeController do
render_views
# login to http basic auth
include AuthHelper
before(:each) do
http_login
end
describe "GET 'index'" do
it "should be successful" do
get 'index'
response.should be_success
end
end
end
Credit here - Archived site
Sorry I didn't seek enough, the solution seems to be the following:
describe "GET 'index'" do
it "should be successful" do
#request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")
get 'index'
response.should be_success
end
end
Some answers suggest to set request.env which is unsafe, because request can be nil and you will end up with private method env' called for nil:NilClass, especially when run single tests with rspec -e
Correct approach will be:
def http_login
user = 'user'
password = 'passw'
{
HTTP_AUTHORIZATION: ActionController::HttpAuthentication::Basic.encode_credentials(user,password)
}
end
get 'index', nil, http_login
post 'index', {data: 'post-data'}, http_login
For me, with Rails 6, I need keyword arguments for rspec get method like .. get route, params: params, headers: headers
Auth Helper method
module AuthHelper
def headers(options = {})
user = ENV['BASIC_AUTH_USER']
pw = ENV['BASIC_AUTH_PASSWORD']
{ HTTP_AUTHORIZATION: ActionController::HttpAuthentication::Basic.encode_credentials(user,pw) }
end
def auth_get(route, params = {})
get route, params: params, headers: headers
end
end
and the rspec request test.
describe HomeController, type: :request do
include AuthHelper
describe "GET 'index'" do
it "should be successful" do
auth_get 'index'
expect(response).to be_successful
end
end
end
When using Rspec to test Grape APIs, the following syntax works
post :create, {:entry => valid_attributes}, valid_session
where valid_session is
{'HTTP_AUTHORIZATION' => credentials}
and
credentials = ActionController::HttpAuthentication::Token.encode_credentials("test_access1")
These are great solutions for controller and request specs.
For feature tests using Capybara, here is a solution to make HTTP Basic authentication work:
spec/support/when_authenticated.rb
RSpec.shared_context 'When authenticated' do
background do
authenticate
end
def authenticate
if page.driver.browser.respond_to?(:authorize)
# When headless
page.driver.browser.authorize(username, password)
else
# When javascript test
visit "http://#{username}:#{password}##{host}:#{port}/"
end
end
def username
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_username
end
def password
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_password
end
def host
Capybara.current_session.server.host
end
def port
Capybara.current_session.server.port
end
end
Then, in your spec:
feature 'User does something' do
include_context 'When authenticated'
# test examples
end
My solution:
stub_request(method, url).with(
headers: { 'Authorization' => /Basic */ }
).to_return(
status: status, body: 'stubbed response', headers: {}
)
Use gem webmock
you can tighten verification by change:
/Basic */ -> "Basic #{Base64.strict_encode64([user,pass].join(':')).chomp}"
URL - can be a regular expression