Achieving 100% test coverage in Rails Oauth using Rspec stubs and mocks - ruby-on-rails

I am trying to figure out a way to stub/mock the access token calls to provide coverage to methods called when a user's token has expired. The more guides I read on this issue the more I get confused. I do not want to call the external provider, and I want to confirm the methods report 100% coverage in case a developer modifies them and they work incorrectly. What should I add to the spec below to make it reach our testing goal of 100%?
The load_json_fixture('omitted_oauth') brings in a JSON fixture based on what the initial Oauth call returns.
Model Concern
module OmittedOmniAuthentication
extend ActiveSupport::Concern
module ClassMethods
def from_omniauth(auth)
Rails.logger.debug auth.inspect
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
setup_user(user, auth)
end
end
def setup_user(user, auth)
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.customer_ids = auth.extra.raw_info.customer_ids
user.store_token(auth.credentials)
end
end
def refresh_token!
access_token ? refresh_access_token! : false
end
def refresh_access_token!
result = access_token.refresh!
store_token(result)
save
rescue OAuth2::Error
false
end
def settings
#settings ||= Devise.omniauth_configs[:omitted].strategy
end
def strategy
#strategy ||= OmniAuth::Strategies::Omitted.new(nil, settings.client_id, settings.client_secret, client_options: settings.client_options)
end
def client
#client ||= strategy.client
end
def access_token
OAuth2::AccessToken.new(client, token, refresh_token: refresh_token)
end
def store_token(auth_token)
self.token = auth_token.token
self.refresh_token = auth_token.refresh_token
self.token_expires_at = Time.at(auth_token.expires_at).to_datetime
end
def token_expired?
Time.now > token_expires_at
end
end
Rspec Spec
RSpec.describe 'OmittedOmniAuthentication', type: :concern do
let(:klass) { User }
let(:user) { create(:user) }
let(:user_oauth_json_response) do
unfiltered_oauth_packet = load_json_fixture('omitted_oauth')
unfiltered_oauth_packet['provider'] = unfiltered_oauth_packet['provider'].to_sym
unfiltered_oauth_packet['uid'] = unfiltered_oauth_packet['uid'].to_i
unfiltered_oauth_packet
end
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:omitted] = OmniAuth::AuthHash.new(
user_oauth_json_response,
credentials: { token: ENV['OMITTED_CLIENT_ID'], secret: ENV['OMITTED_CLIENT_SECRET'] }
)
end
describe "#from_omniauth" do
let(:omitted_oauth){ OmniAuth.config.mock_auth[:omitted] }
it 'returns varying oauth related data for Bigcartel OAuth response' do
data = klass.from_omniauth(omitted_oauth)
expect(data[:provider]).to eq(user_oauth_json_response['provider'].to_s)
expect(data[:uid]).to eq(user_oauth_json_response['uid'].to_s)
expect(data[:email]).to eq(user_oauth_json_response['info']['email'])
expect(data[:customer_ids]).to eq(user_oauth_json_response['extra']['raw_info']['customer_ids'])
end
end
describe '#token expired?' do
it 'true if valid' do
expect(user.token_expired?).to be_falsey
end
it 'false if expired' do
user.token_expires_at = 10.days.ago
expect(user.token_expired?).to be_truthy
end
end
end
UPDATE
describe '#refresh_access_token!' do
it 'false if OAuth2 Fails' do
allow(user).to receive(:result).and_raise(OAuth2::Error)
expect(user.refresh_access_token!).to be_falsey
end
it 'false if refresh fails' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { false }
expect(user.refresh_token!).to be_falsey
end
it 'true if new token' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { true }
expect(user.refresh_token!).to be_truthy
end
it 'true when refreshed' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { true }
allow(user).to receive(:store_token) { true }
allow(user).to receive(:save) { true }
expect(user.refresh_access_token!).to be_truthy
end
end
=> I was able to get to 94.12% with these updates

I'm not sure where you might be calling the external provider, so I'm not sure what you want to stub/mock.
To get you a little closer to your coverage goal, try adding another spec for your simplest module methods:
describe '#refresh_token!' do
it 'is true if there is an access_token' do
if !user.access_token?
expect(user.refresh_token!).to be_truthy
end
end
# Do you have factories or fixtures set up that can force
# #access_token? to be falsey?
it 'is false if there is no access_token' do
if !user.access_token?
expect(user.refresh_token!).to be_falsey
end
end
# Maybe you want to set the falsey value for the access_token
# as you have have for the value of token_expires_at in
# your #token_expired? test.
it 'is false if there is no access_token' do
# You should be able to force the method to return a false
# value (stub the method) with this line
allow(user).to receive(:access_token) { false }
expect(user.refresh_token!).to be_falsey
end
end
This example feels a little unnecessary since your access_token method appears that it will never return false. I would expect that your access_token method will always return an object, or an error, so your refresh_token! method would never encounter a falsey condition in the ternary. Maybe you should instead rescue and return false.
Regardless, I think the point is that you should stub the method with the allow method, and that will get you on your way to figuring out your method stubs. Hope it helps somewhat.
For refresh_access_token! you can unit test the method by stubbing the user.result method with an error, and not stubbing for the "successful" result of the refresh_access_token! method.
describe '#refresh_access_token!' do
it 'it returns true when refreshed' do
# The successful control flow path for this method
# is to save the user and return true.
# I suppose this would happen smoothly in your tests and app.
expect(user.refresh_access_token!).to be_truthy
end
it 'returns false when an OAuth2 Error is rescued' do
# To force the case that you receive an OAuth2 Error,
# stub the user's access_token return value with the Error
# The refresh_access_token! method should then rescue the error
# and cover the false return value of the method
allow(user).to receive(:access_token) { OAuth2::Error }
expect(user.refresh_access_token!).to be_falsey
end
end

(Posted solution on behalf of the question author).
This is now working. With the following spec adjustment stubbing the method chain I was able to get a successful call of true for the method:
def refresh_access_token!
result = access_token.refresh!
store_token(result)
save
rescue OAuth2::Error
false
end
The completed spec that pushed me to 100%
it 'true when refreshed' do
auth_token = OpenStruct.new(token: FFaker::Lorem.characters(50),
refresh_token: FFaker::Lorem.characters(50),
expires_at: 5.days.from_now)
allow(user).to receive_message_chain('access_token.refresh!') { auth_token }
expect(user.refresh_access_token!).to be_truthy
end
Stubs and Mocks can be fun. I learned a ton from this thread. Here are the Rspec 3.4 docs on this.

Related

Mock a singleton class In Ruby on Rails using Rspec [SOLVED]

Hi I Try to create a mock for follow class:
module EstablishmentsQueryService
class << self
def find_by_id(id)
Establishment.find_by!(id:)
rescue ActiveRecord::RecordNotFound
raise EstablishmentNotFoundError.new id
end
end
end
to try test my controller
# frozen_string_literal: true
module Api
module V1
# Controllewr to manager Establishments
class EstablishmentsController < Api::V1::ApiController
before_action :validate_id, only: %i[destroy update show]
before_action :load_establishment, only: %i[destroy update show]
def show; end
def create
#establishment = Establishment.new(establishment_params)
#establishment = EstablishmentService.save(#establishment)
render status: :created
end
def destroy
EstablishmentService.delete(#establishment)
end
def update
#establishment.attributes = establishment_params
#establishment = EstablishmentService.save(#establishment)
end
private
def validate_id
message = I18n.t('establishment_controller.id.invalid', id: params[:id])
UuidValidateService.call(params[:id], message)
end
def load_establishment
#establishment = EstablishmentsQueryService.find_by_id(params[:id])
end
def establishment_params
params.require(:establishment).permit(:name, :cnpj, :description)
end
end
end
end
follow my test:
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Establishments', type: :request do
describe 'GET /api/v1/establishments/:id' do
context 'when has establishment' do
let(:establishment) { build(:establishment, id: p, created_at: DateTime.now, updated_at: DateTime.now) }
before do
allow_any_instance_of(EstablishmentsQueryService).to receive(:find_by_id).and_return(establishment)
get "/api/v1/establishments/#{establishment.id}"
end
it 'then http status is ok' do
expect_status_is_ok
end
it 'has body equal to expected' do
except_field_by_field(establishment, body_to_open_struct, %i[id name cnpj description])
end
end
context 'when has no establishment' do
before do
get "/api/v1/establishments/#{UUID.new.generate}"
end
it 'then http status is not_found' do
expect_status_is_not_found
end
end
context 'when use invalid id' do
before { get "/api/v1/establishments/#{FFaker::Lorem.word}" }
it 'then http status is bad_request' do
expect_status_is_bad_request
end
end
end
describe 'PUT /api/v1/establishments/:id' do
let(:establishments_query_service) { allow(EstablishmentsQueryService) }
let(:establishments_service) { allow(EstablishmentsService) }
context 'when updated with success' do
let(:establishment) { build(:establishment) }
let(:id) { UUID.new.generate }
before do
establishments_query_service.to receive(:find_by_id) { |p| build(:establishment, id: p, created_at: DateTime.now, updated_at: DateTime.now) }
establishments_service.to receive(:save) do |p|
to_return = p
to_return.created_at = DateTime.now
to_return.updated_at = DateTime.now
end
put "/api/v1/establishments/#{id}"
end
it 'then http status is ok' do
expect_status_is_ok
end
it 'has body equal to expected' do
actual = body_to_open_struct
except_field_by_field(establishment, actual, %i[name cnpj description])
expected(actual.id).to eq(id)
end
end
context 'when has no establishment' do
end
context 'when has constraint violation' do
end
end
describe 'DELETE /api/v1/establishments/:id' do
end
describe 'POST /api/v1/establishments' do
end
end
If I work using allow_any_instance_of a test ignore configuration, use a real configuration and fails because has no data stores. If I use double I received a follow error:
Api::V1::Establishments GET /api/v1/establishments/:id when has establishment then http status is ok
Failure/Error: allow_any_instance_of(EstablishmentsQueryService).to receive(:find_by_id).and_return(establishment)
EstablishmentsQueryService does not implement #find_by_id
I think the right away is user allow_any_instance_of because this config is for static methods, but didn't work
how can I mock my class to test my controller? I using Ruby 3.1.2, rails 7.0.3 and rspec-rails 5.1.2
thank you
I found my problem, I forgot to definie expected params in my confi using with()
allow(EstablishmentsQueryService).to receive(:find_by_id).with(establishment.id).and_return(establishment)

Using RSpec to test a Retry RestClient

I'm using Oauth so what I do is store access_token and refresh token at User table, I create some classes to do this. In the Create class I do the normal functionality of the code (create records on the integration). The access_token expire at 1 hour, so intead of schedule an active job to refresh that token at that time I decided to do Refresh.new(user).call to request a new access_token and refresh_token.
I know that code works, because I've tested on live and I'm getting the new token when the access_token is expired. But I want to do a rspec test for this.
part of my rspec test:.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized).once
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
This is the response:
(RestClient).post(# data)
expected: 1 time with any arguments
received: 2 times with arguments: (# data)
This is my code:
Create.rb
class Create
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
create_contact
rescue RestClient::Unauthorized => ex
retry if Refresh.new(user).call
rescue RestClient::ExceptionWithResponse => ex
logger.error(#message.api_error(ex))
raise
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def create_contact
response = RestClient.post(
url, contact_params, contact_headers
)
logger.info(#message.api_response(response))
end
end
Refresh.rb
class Refresh
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
refresh_authorization_code
end
def refresh_authorization_code
response = RestClient.post(url, authorization_params)
logger.info(#message.api_response(response))
handle_response(response)
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def handle_response(response)
parsed = JSON.parse(response)
user.update!(access_token: parsed[:access_token], refresh_token: parsed[:refresh_token])
end
end
Also I tried using something like this from here
errors_to_raise = 2
allow(RestClient).to receive(:get) do
return rest_response if errors_to_raise <= 0
errors_to_raise -= 1
raise RestClient::Unauthorized
end
# ...
expect(client_response.code).to eq(200)
but I don't know how handle it propertly.
Your test calls RestClient.post twice, first in Create then again in Retry. But you only mocked one call. You need to mock both calls. The first call raises an exception, the second responds with a successful result.
We could do this by specifying an order with ordered...
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized)
.ordered
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.and_return(auth_response)
.ordered
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
However, this makes a lot of assumptions about exactly how the code works, and that nothing else calls RestClient.post.
More robust would be to use with to specify responses with specific arguments, and also verify the correct arguments are being passed.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_raise(RestClient::Unauthorized)
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_return(auth_response)
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
But this still makes a lot of assumptions about exactly how the code works, and you need to make a proper response.
Better would be to focus in on exactly what you're testing: when the create call gets an unauthorized exception it tries to refresh and does the call again. This unit test doesn't have to also test that Refresh#call works, just that Create#call calls it. You don't need to have RestClient.post raise an exception, just that Create#create_contact does.
context 'when token is expired' do
it 'requests a refresh token and retry' do
old_key = user.access_token
create = Create.new(user)
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It refreshes
refresh = double
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
# The refresh succeeds
expect(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
# It tries again
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
And you can also test when the retry fails. These can be combined together.
context 'when token is expired' do
let(:refresh) { double }
let(:create) { Create.new(user) }
before {
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It tries to refresh
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
}
context 'when the refresh succeeds' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
}
it 'retries' do
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
context 'when the refresh fails' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(false)
}
it 'does not retry' do
expect(create)
.not_to receive(:create_contact)
.ordered
create.call
end
end
end

Authenticated Request specs

I understand this question has been asked before in various forms.
However I'm struggling with something that doesn't seem to be being solved in these answers. My logged in user is not persisting within the spec.
How are you supposed to replicate authentication/logged in users in a request spec?
Here's what I've tried, and what I'm doing.
I'm using Auth0 as my authentication handler. I have a signin method that's called in the Auth0 Callback, so I've jerry-rigged a mock_auth endpoint for my tests to utilize a resource object.
This is my current set up and what I've done to try and replicate the login flow.
#/spec/requests/api/v1/account_spec.rb
RSpec.describe "API V1 Accounts", type: :request do
# Factories.
...
describe "PATCH update" do
subject(:http_request) { patch endpoint, params: { account: account_params, format: :json } }
# set some defaults
let(:id) { account.id }
let(:endpoint) { "/api/v1/accounts/#{id}" }
let(:account_params) { {} }
# Configure subdomain contstraint
within_subdomain :api do
before do |example|
mock_login(resource) unless example.metadata[:skip_signin]
http_request
end
context "when no resource is logged in", :skip_signin do
# This spec passes fine, as it's skipping login.
it_behaves_like "an unauthenticated private api request"
end
context "when there is no record to be found" do
let(:id) { SecureRandom.uuid }
let(:resource) { create(:user) }
it "fails to access a record" do
expect(response).to have_http_status(:not_found)
end
end
xcontext "when the user has access permission" do
end
end
end
end
-
# config/routes.rb
post "/auth/mock/:id", to: "auth#mock", as: :mock_login if Rails.env.test?
-
# auth_controller.rb
def mock
return unless Rails.env.test?
#resource = User.find_by(params[:id]
signin(#resource)
end
def signin(resource)
reset_session
create_session(resource)
after_signin_redirect_for(resource)
end
and I'm using this helper to call it from my request spec
module Helpers
module Auth
def mock_login(resource)
post mock_login_path(resource.id)
end
end
end
RSpec.configure do |config|
config.include Helpers::Auth, type: :request
end
So. By throwing around a bunch of debuggers and binding.pry I can see that my mock_login(resource) is being called successfully and at the end of the signin method, my helper signed_in? is true. Having successfully set a session.
The issue that I'm having now, is that this is not persisting in the feature spec when it's run in the before block, or in the it block.
before do |example|
mock_login(resource) unless example.metadata[:skip_signin] # signed_in? == true!
http_request # signed_in? == nil
end
module API
module V1
class AccountsController < APIController
before_action :authenticate_resource!
# ^ This is where the spec is failing to recognise the signed in resource from the mock_login method.
before_action :set_account
# PATCH /api/v1/accounts/:id
def patch_update
# Cancancan Authorization
authorize! :update, #account
# handle patch
...
end
private
def set_account
binding.pry # We're never making it here.
#account = Account.find_by(id: params[:id])
end
...
end
end
end
def authenticate_resource!
return true if signed_in?
respond_to do |format|
format.json { head(:unauthorized) }
end
end
EDIT: A couple of changes to make it clearer what I'm asking.

Rails 5 Rspec set session variable

I have a request test spec which tests a POST request.
In my PaymentController (which handles the POST request), i have
before_action :require_user, :require_order
which basically gets the user and order based on the session.
Any idea how i can set session variable(user_id and order_id) in my request test spec?
Since Rails 5.0 the best way is to use the keyword arguments in the controller/request tests:
get :index, params: { ... }, session: { user_id: 1, ... }
If you are using a authentication library like Devise, Clearance and such, there are various helpers to stub a logged in user, see here the Documentation for Devise:
before(:each) do
# or def setup if using minitest
#request.env["devise.mapping"] = Devise.mappings[:user]
sign_in User.create(...)
end
For Rails < 5, this solution works
def _set_session(key, value)
Warden.on_next_request do |proxy|
proxy.raw_session[key] = value
end
end
_set_session(:access_acount_id, user.id)
Example
def _set_session(key, value)
Warden.on_next_request do |proxy|
proxy.raw_session[key] = value
end
end
context 'When there is something' do
let(:yesterday) { 1.day.ago }
let(:updates_enabled) { true }
before do
_set_session(:access_acount_id, user.id)
login_as(user, scope: :user)
end
it 'does not update records' do
visit dashboard_path
expect(page).to have_css('.popup#my-popup')
end
end

RSpec Request - How to set http authorization header for all requests

I'm using rspec request to test a JSON API that requires an api-key in the header of each request.
I know I can do this:
get "/v1/users/janedoe.json", {}, { 'HTTP_AUTHORIZATION'=>"Token token=\"mytoken\"" }
But it is tedious to do that for each request.
I've tried setting request.env in the before block, but I get the no method NilClass error since request doesn't exist.
I need some way, maybe in the spec-helper, to globally get this header sent with all requests.
To set it in a before hook you need to access it like
config.before(:each) do
controller.request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials('mytoken')
end
I too hated the giant hash, but preferred to be explicit in authorizing the user in different steps. After all, it's a pretty critical portion, and . So my solution was:
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
end
end
#spec/spec_helper.rb
RSpec.configure do |config|
...
config.include ControllerSpecHelpers, :type => :controller
then I can use it like so
describe Api::V1::Users, type: :controller do
it 'retrieves the user' do
user = create :user, name: "Jane Doe"
authorize user
get '/v1/users/janedoe.json'
end
end
I find this great for testing different authorization levels. Alternatively, you could have the helper method spec out the authorize function and get the same result, like so
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate
controller.stub(:authenticate! => true)
end
end
However, for ultimate speed and control, you can combine them
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user = nil
if user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
else
controller.stub(:authenticate! => true)
end
end
end
and then authorize entire blocks with
#spec/spec_helper.rb
...
RSpec.configure do |config|
...
config.before(:each, auth: :skip) { authenticate }
#**/*_spec.rb
describe Api::V1::Users, type: :controller do
context 'authorized', auth: :skip do
...
I know that this question has already been answered but here's my take on it. Something which worked for me:
request.headers['Authorization'] = token
instead of:
request.env['Authorization'] = token
This is another way to do it if you are doing a post.
#authentication_params = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(Temp::Application.config.api_key) }
expect { post "/api/interactions", #interaction_params, #authentication_params }.to change(Interaction, :count).by(1)
Note interaction_params is just a json object I am passing in.
I don't think you should depend on the header if you are not testing the header itself, you should stub the method that checks if the HTTP_AUTORIZATION is present and make it return true for all specs except the spec that tests that particular header
something like...
on the controller
Controller...
before_filter :require_http_autorization_token
methods....
protected
def require_http_autorization_token
something
end
on the spec
before(:each) do
controller.stub!(:require_http_autorization_token => true)
end
describe 'GET user' do
it 'returns something' do
#call the action without the auth token
end
it 'requires an http_autorization_token' do
controller.unstub(:require_http_autorization_token)
#test that the actions require that token
end
end
that way one can forget the token and test what you really want to test

Resources