I have this controller concern which accesses URL parameters and request headers.
module Authentication
extend ActiveSupport::Concern
# This concern has all authentication related functionality.
# This should be extended into ApplicationController
def current_user
# Returns a user from a request header containing a session token or nil
# or a URL parameter containing the same
token = request.headers[:token] || params[:token]
session = Session.find_by_token(token)
session.user if session
end
def authenticated?
# Is the user authenticated?
# This makes no assumptions regarding that the user is privileged enough to do something
return true if current_user
end
end
I'm unsure of how to test this in RSpec. How do I do this?
You can try shared example
# spec/support/authentication.rb
shared_examples "authentication" do
# Your tests here
let(:token) { "RandomString" }
let(:user) {create(:user)}
let(:session) {create(:session, token: token, user_id: user.id)}
describe "#authenticated?" do
it "should be authenticated" do
session.user.should eq(user)
end
end
end
# spec/lib/authentication_spec.rb
module TestAuths
class Authenticate
include Authentication
end
end
describe Authentication do
context "inclusion of the module" do
subject(:with_auth) { TestAuths::Authenticate.new }
it_behaves_like "authentication"
end
end
Related
It'd be really handy to know if this is correct or a bit off before I start doing this all over the place.
Im trying to set up an API and I want to be able to access current_user in my controllers. So I'm setting up some authentication, which i'm okay with it being basic for for now while i develop. I want to develop with tests and I've done this
spec/requests/api/v1/topics_spec.rb
RSpec.describe 'API::V1::Topics API', type: :request do
let!(:user) { create(:user, permission: "normal") }
let!(:user_encoded_credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(user.email, user.password) }
let(:headers) { { "ACCEPT" => "application/json", Authorization: user_encoded_credentials } }
it 'returns some topics' do
get '/api/v1/topics', headers: headers
expect(response).to have_http_status(:success)
end
It seems a bit weird having to call "let!" for each user and encoded credentials at the top. I feel like there might be a better way but cant seem to find it by googling.
My plan is to add this code every time I create a test user so I can pass the correct basic authentication header with each request.
Heres the api_controller code if needed also:
app/controllers/api/v1/api_controller.rb
module Api
module V1
class ApiController < ActionController::Base
before_action :check_basic_auth
skip_before_action :verify_authenticity_token
private
def check_basic_auth
unless request.authorization.present?
head :unauthorized
return
end
authenticate_with_http_basic do |email, password|
user = User.find_by(email: email.downcase)
if user && user.valid_password?(password)
#current_user = user
else
head :unauthorized
end
end
end
def current_user
#current_user
end
end
end
end
One way of handling this is to create a simple helper method that you include into your specs:
# spec/helpers/basic_authentication_test_helper.rb
module BasicAuthenticationTestHelper
def encoded_credentials_for(user)
ActionController::HttpAuthentication::Basic.encode_credentials(
user.email,
user.password
)
end
def credentials_header_for(user)
{ accept: "application/json", authorization: encoded_credentials_for(user) }
end
end
RSpec.describe 'API::V1::Topics API', type: :request do
# you can also do this in rails_helper.rb
include BasicAuthenticationTestHelper
let(:user) { create(:user, permission: "normal") }
it 'returns some topics' do
get '/api/v1/topics', **credentials_header_for(user)
expect(response).to have_http_status(:success)
end
end
You can create wrappers for the get, post, etc methods that add the authentication headers if you're doing this a lot.
Not all your test setup actually belongs in let/let! blocks. Its often useful to define actual methods that take input normally. Resuing your spec setup can be done either with shared contexts or modules.
The more elegant solution however is to make your authentication layer stubbable so you can just set up which user will be logged in even without the headers. Warden for example allows this simply by setting Warden.test_mode! and including its helpers.
Its right way to create let or let! each time on top and define it on your test.
But, if you want use best practices in your code, you can stub request once and use it later only with one method, without affecting real requests
def stub_my_request
stub_request(:post, '/api/v1/topics').with(headers: headers).and_return(status: 200, body: body_from_your_let)
end
And use it in you tests
context "context" do
it "do smth" do
stub_my_request
response = get '/api/v1/topics', headers: headers
expect(response).to have_http_status(:success)
end
end
let me directly get into the solution you are looking for.
Create support file in spec/supports/helper.rb.
And make sure to load the helper in spec/rails_helper.rb
Dir[Rails.root.join('spec', 'supports', '**', '*.rb')].each(&method(:require))
Inside spec/supports/helper.rb paste the below code. This is based on cookies-based authentication using devise. But if you are using JWT then just return the token returned from the authentication.
def sign_in(user)
post user_session_url, params: { user: { login: user.email, password: user.password } }
response.header
end
def sign_out
delete destroy_user_session_url
end
Then in your spec file just use like below:
RSpec.describe '/posts', type: :request do
let(:user) { create(:admin) }
before(:each) do
sign_in(user)
end
# Your test case starts from here.
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 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
My current code looks like this:
/spec/support/spec_test_helper.rb
module SpecTestHelper
def login_admin
user = FactoryGirl.create(:user, type: 0)
session[:user_id] = user.id
end
end
/app/controllers/application_controller.rb
def current_user
if session[:user_id].nil?
render plain: 'Error', status: :unauthorized
else
#current_user ||= User.find(session[:user_id])
end
end
Unfortunately, session is always empty in the current_user method. Is there a way of controlling the session through RSpec?
This will change based on the spec type. For example, a feature spec will not allow you to directly modify the session. However, a controller spec will.
You will need to include the helper methods module into your example group. Say you have a WidgetsController:
require 'support/spec_test_helper'
RSpec.describe WidgetsController, type: :controller do
include SpecTestHelper
context "when not logged in" do
it "the request is unauthorized" do
get :index
expect(response).to have_http_status(:unauthorized)
end
end
context "when logged in" do
before do
login_admin
end
it "lists the user's widgets" do
# ...
end
end
end
You can also automatically include the module into all specs, or specific specs by using metadata.
I often do this by adding the configuration changes into the file which defines the helper methods:
/spec/support/spec_test_helper.rb
module SpecTestHelper
def login_admin
user = FactoryGirl.create(:user, type: 0)
session[:user_id] = user.id
end
end
RSpec.configure do |config|
config.include SpecTestHelper, type: :controller
end
I'm using rspec request to test a JSON API that requires an api-key in the header of each request.
I know I can do this:
get "/v1/users/janedoe.json", {}, { 'HTTP_AUTHORIZATION'=>"Token token=\"mytoken\"" }
But it is tedious to do that for each request.
I've tried setting request.env in the before block, but I get the no method NilClass error since request doesn't exist.
I need some way, maybe in the spec-helper, to globally get this header sent with all requests.
To set it in a before hook you need to access it like
config.before(:each) do
controller.request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials('mytoken')
end
I too hated the giant hash, but preferred to be explicit in authorizing the user in different steps. After all, it's a pretty critical portion, and . So my solution was:
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
end
end
#spec/spec_helper.rb
RSpec.configure do |config|
...
config.include ControllerSpecHelpers, :type => :controller
then I can use it like so
describe Api::V1::Users, type: :controller do
it 'retrieves the user' do
user = create :user, name: "Jane Doe"
authorize user
get '/v1/users/janedoe.json'
end
end
I find this great for testing different authorization levels. Alternatively, you could have the helper method spec out the authorize function and get the same result, like so
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate
controller.stub(:authenticate! => true)
end
end
However, for ultimate speed and control, you can combine them
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user = nil
if user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
else
controller.stub(:authenticate! => true)
end
end
end
and then authorize entire blocks with
#spec/spec_helper.rb
...
RSpec.configure do |config|
...
config.before(:each, auth: :skip) { authenticate }
#**/*_spec.rb
describe Api::V1::Users, type: :controller do
context 'authorized', auth: :skip do
...
I know that this question has already been answered but here's my take on it. Something which worked for me:
request.headers['Authorization'] = token
instead of:
request.env['Authorization'] = token
This is another way to do it if you are doing a post.
#authentication_params = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(Temp::Application.config.api_key) }
expect { post "/api/interactions", #interaction_params, #authentication_params }.to change(Interaction, :count).by(1)
Note interaction_params is just a json object I am passing in.
I don't think you should depend on the header if you are not testing the header itself, you should stub the method that checks if the HTTP_AUTORIZATION is present and make it return true for all specs except the spec that tests that particular header
something like...
on the controller
Controller...
before_filter :require_http_autorization_token
methods....
protected
def require_http_autorization_token
something
end
on the spec
before(:each) do
controller.stub!(:require_http_autorization_token => true)
end
describe 'GET user' do
it 'returns something' do
#call the action without the auth token
end
it 'requires an http_autorization_token' do
controller.unstub(:require_http_autorization_token)
#test that the actions require that token
end
end
that way one can forget the token and test what you really want to test