1

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

halfer
  • 19,824
  • 17
  • 99
  • 186
Chris Hough
  • 3,389
  • 3
  • 41
  • 80
  • This is a little too big and broad for me to read and understand. Can you point out a specific line or lines that you want to cover? – Dave Schweisguth Jun 16 '16 at 13:26
  • @DaveSchweisguth I have updated the question with coverage issues. – Chris Hough Jun 16 '16 at 15:10
  • So, your image there is your test coverage? Is the `token_expired?` method covered? In other words, is `Time.now > token_expires_at` green? It's not visible in your image. – sealocal Jun 17 '16 at 17:29
  • I have test coverage on that method, but the others are not @sealocal – Chris Hough Jun 17 '16 at 17:31
  • @sealocal from my research I have to stub | mock and I am pulling my hair out – Chris Hough Jun 17 '16 at 17:35
  • Well, you've explicitly called `user.token_expired?`, so it makes sense that it's covered. I'm guessing that you've included the `OmittedOmniAuthentication` module in your User model? So, I would try adding another `describe` block like `describe '#refresh_token?' do` and add a spec that calls `refresh_token?` on your `user` instance. Maybe that's in conflict with the approach you desire? – sealocal Jun 17 '16 at 17:42
  • @sealocal I get that in theory, but do you have an example that would mock this out? I am not 100% sure as to what you are pointing too, thank you so much for your help – Chris Hough Jun 17 '16 at 17:57
  • @sealocal I also, want to make sure to give you credit for helping – Chris Hough Jun 17 '16 at 17:57

2 Answers2

0

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
sealocal
  • 10,897
  • 3
  • 37
  • 50
  • I am in the final stretch + I just made some major edits. Thoughts on getting to 100%? – Chris Hough Jun 18 '16 at 01:05
  • It looks like for every test that calls `refresh_access_token!`, you've either stubbed that method, or stubbed the `user.result`, so the `refresh_access_token!` method never fully executes. You could write a describe block for the `refresh_access_token!`. Perhaps for one test in the describe block you could test the successful condition and for the other you could stub the result as the error that you are rescuing. – sealocal Jun 18 '16 at 01:30
  • ok, maybe I am lost here @sealocal, how would I write what you are saying? – Chris Hough Jun 18 '16 at 01:45
  • I added the code example for unit testing your `refresh_access_token!` method. – sealocal Jun 18 '16 at 02:46
  • I tried your examples and still not hitting 100%. I updated my example to the latest as well under "updated" above. When I tried the `expect(user.refresh_access_token!).to be_truthy` it failed b/c the method returned false? I am still hacking on this. Thoughts? – Chris Hough Jun 19 '16 at 05:12
  • There's only three main lines in your refresh_access_token! method. Either the first line is returning false or the save is returning false. You can use save! to raise an error instead of return false, which will more quickly tell you if your user instance is invalid upon save. You should see the error as output from your test run. If your user is valid, then determine why the first line is returning false. Perhaps you insert puts lines to output the state of variables and the method return value. – sealocal Jun 19 '16 at 05:48
  • thank you for your help. I gave you credit for this answer and please check out my final update. It was the stub chain that I needed to get the method to reach 100%. – Chris Hough Jun 19 '16 at 21:11
  • I have a follow up to this http://stackoverflow.com/questions/38085404/rails-4-oauth-model-concern-test-coverage-stubs I was wondering if you could offer insight on? – Chris Hough Jun 28 '16 at 20:10
0

(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.

halfer
  • 19,824
  • 17
  • 99
  • 186