简体   繁体   中英

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

在此处输入图片说明

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM