It is likely that you have an authentication layer behind your Rails’ controllers, for example some actions are only available for logged in users. For this example let’s suppose you let authenticated users create likes and comments on the app.
A simple form of authentication could be to pass a token on the request headers as shown below. There are other options for authentication but we’ll keep it simple for now.
class Api::BaseController < ActionController::Base
before_action :validate_access_token
def validate_access_token
return unauthorized unless authenticate_or_request_with_http_token do |token, _|
ActiveSupport::SecurityUtils.secure_compare(token, Rails.application.secrets.access_tokens[:api_token])
end
end
end
Then the controllers would extend from this base controller, inehriting the authentication part.
class Api::CommentsController < Api::BaseController
def create
# creation code
end
end
class Api::LikesController < Api::BaseController
def create
# creation code
end
end
Using Shared Examples on Rspec
Now you when writing tests for these controllers, you should test the authentication layer. For this I like to use rspec shared_examples where you can reuse the same tests through several spec files. However, when using you have to be disciplined and keep the same name for your variables. In the examples below call_action is the controller action.
All the tests in the shared_examples test all possible scenarios regarding authenticated requests. I personally like to place these files under spec/support. To load these files you can add this to your rails_helper:
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
shared_examples_for "an action with a access token authentication" do
context "with a valid access token" do
before do
http_authorization_header(access_token)
call_action
end
it { expect(response.status).to_not eq(401) }
end
context "with an invalid access token" do
before do
http_authorization_header("invalid_token")
call_action
end
it "returns 401 (unauthorized)" do
expect(response.status).to eq(401)
end
end
context "without an access token" do
before do
http_authorization_header(nil)
call_action
end
it "returns 401 (unauthorized)" do
expect(response.status).to eq(401)
end
end
end
shared_examples_for "an action that does not return an error" do
it do
call_action
expect(response).to_not have_http_status(:error)
end
end
def http_authorization_header(access_token)
request.env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Token.encode_credentials(access_token)
end
So on your controller spec files you would only need to call itbehaveslike and use the name you defined on the shared_examples file.
require 'rails_helper'
describe Api::LikesController, type: :controller do
let(:access_token) { Rails.application.secrets.access_tokens[:api_token] }
before { http_authorization_header(access_token) }
describe "#create" do
let(:call_action) { post :create, params: params }
it_behaves_like "an action with a access token authentication"
end
end
It is not mandatory to keep the sharedexamples in separate files, you can declare them directly on the specfile. I just place them on the support directory because I am reusing it through several controller spec files.