chrishough chrishough - 5 months ago 42
Ruby Question

rails 4 oauth model concern rspec stubs and mocks

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. Can someone point me in the right direction on what to 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


enter image description here

UPDATE ONE

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

enter image description here

UPDATE TWO => Final and 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 rpsec 3.4 docs on this.

Answer

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