Home > Mobile >  How should I set up basic authentication headers in RSpec tests?
How should I set up basic authentication headers in RSpec tests?

Time:08-20

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 

CodePudding user response:

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 usefull 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.

CodePudding user response:

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

CodePudding user response:

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
  • Related