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.