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