I have a rails-api application that I'm testing using Rspec. The Application uses devise_token_auth gem for authentication and cancancan gem for authorization.
devise_token_auth requires that the client include these authentication headers in every request:
access-token
, client
, expiry
, uid
. These headers are available in a response after successful authentication using email
and password
.
I have decided to use a solution provided in this answer to set these headers during testing.
In ability.rb
I have this:
## models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.role? :registered
can :create, Post, user_id: user.id
can :update, Post, user_id: user.id
can :destroy, Post, user_id: user.id
can :read, Post
end
end
end
posts#show
action in PostsController
looks like this:
## controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource
def show
render json: @post
end
end
I have rescued CanCan::AccessDenied
error to render a json message and a status of 403
forbidden
in ApplicationController
rescue_from CanCan::AccessDenied do |exception|
render json: {"message" => "unauthorized"}.to_json, :status => 403
end
I have this in spec/support/session_helper.rb
module SessionHelper
def retrieve_access_headers
##I have a user with these credentials and has a "registered" role. in the the test db.
post "/auth/sign_in", params: {:email => "registered_user@gmail.com", :password => "g00dP@ssword"}, headers: {'HTTP_ACCEPT' => "application/json"}
##These two pass
expect(response.response_code).to eq 200
expect(response.body).to match(/"email": "registered_user@gmail.com"/)
access_headers = {"access-token" => response.headers["access-token"],
"client" => response.headers["client"],
"expiry" => response.headers["expiry"],
"uid" => response.headers["uid"],
"token-type" => response.headers["token-type"],
'HTTP_ACCEPT' => "application/json"
}
return access_headers
end
end
I have this in spec/support/requests_helper.rb
module RequestsHelper
def get_with_token(path, params={}, headers={})
headers.merge!(retrieve_access_headers)
get path, params: params, headers: headers
#### this outputs the expected headers on a json string and they seem fine ####
puts "headers: "+headers.to_json
end
end
I have included the two helpers in rails_helper.rb
as shown below:
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
config.include SessionHelper, type: :request
config.include RequestsHelper, type: :request
end
Finally I have a request spec in spec/request/posts/show_spec.rb
require 'rspec/its'
require 'spec_helper'
require 'rails_helper'
RSpec.describe 'GET /posts/:id', :type => :request do
let(:post) {create(:post)}
let(:id) {post.id}
let(:request_url) {"/posts/#{id}"}
context 'with a registered user' do
it 'has a status code of 200' do
get_with_token request_url
expect(response).to have_http_status(:success)
end
end
end
I expect this to pass but it fails with message:
Failure/Error: expect(response).to have_http_status(:success)
expected the response to have a success status code (2xx) but it was 403
The application works as expected on a browser.
What I'm a doing wrong?