Search code examples
ruby-on-railsrubyrspecomniauth

Achieving 100% test coverage in Rails Oauth using 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. 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

enter image description here

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

enter image description here


Solution

  • 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