Search code examples
ruby-on-railsapirspecbasic-authentication

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


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 

Solution

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