Rspec stubbing a constant set to a Rails credential

111 Views Asked by At

I have a class:

class Vendor::Connection
  VENDOR_CLIENT_ID = Rails.application.credentials.vendor_api[:client_id].freeze
  VENDOR_CLIENT_SECRET = Rails.application.credentials.vendor_api[:client_secret].freeze

  # ...

  def token_body
    {
      client_id: VENDOR_CLIENT_ID,
      client_secret: VENDOR_CLIENT_SECRET,
    }
  end
end

When I attempt to mock the credentials using any of the methods in this Rspec Github Issue

require 'rails_helper'

describe Vendor::Connection do
  before do
    allow(Rails.application.credentials).to receive(:vendor_api).and_return({ client_id: '123' })
  end

  it 'works' do
    expect(true).to eq(true)
  end
end

I receive:

Failure/Error: VENDOR_CLIENT_ID = Rails.application.credentials.vendor_api[:client_id]

NoMethodError:
  undefined method `[]' for nil:NilClass

I also tried a before block of:

  before do
    stub_const('Vendor::Connection::VENDOR_CLIENT_ID', '123')
    stub_const('Vendor::Connection::VENDOR_CLIENT_SECRET', '456')
    stub_const('Vendor::Connection::VENDOR_PARTNER_ID', '789')
  end

I still receive:

Failure/Error: VENDOR_CLIENT_ID = Rails.application.credentials.vendor_api[:client_id]

NoMethodError:
  undefined method `[]' for nil:NilClass

I tried:

shared_context 'credentials' do
  before do
    allow(Rails.application).to receive(:credentials).and_return(OpenStruct.new(vendor_api: {client_id: '123', client_secret: '456', partner_id: '789'}))
  end
end

describe Vendor::Connection do
  include_context 'credentials'

  it 'works' do
    expect(true).to eq(true)
  end
end

Which also returned:

Failure/Error: VENDOR_CLIENT_ID = Rails.application.credentials.vendor_api[:client_id]

NoMethodError:
  undefined method `[]' for nil:NilClass

I also attempted to put a binding.pry as the first line in the before block, but it fails before getting to the binding.pry

Running on: ruby 3.0.5 rails 6.1.7.4 rspec 3.10.0 rspec-rails 4.1.2

Any help would be greatly appreciated

2

There are 2 best solutions below

3
mechnicov On BEST ANSWER

I suggest to use different credentials for different environments and do not stub them

rails credentials:edit --environment test

The above command does the following:

  • creates config/credentials/test.key if missing

  • creates config/credentials/test.yml.enc if missing

  • decrypts and opens test credentials file in the default editor

Since that credentials and key are for tests only, you can commit both files with Git and use them in CI if needed

1
Siim Liiser On

Make sure your constants are actually constant.

In this case, the constant VENDOR_CLIENT_ID is not constant because it depends on the value from secrets.

When loading code, and Ruby reaches the VENDOR_CLIENT_ID = ... line, it immediately tries to populate the value by accessing the secrets. This happens during code loading before any of your code is actually executed.

I see 2 ways to fix this.

  1. The hacky way. Change your code loading order so that this class is only loaded when it's needed and not load it until your RSpec mocks have been set.
  2. Don't use a constant for values that are not constant. Change the VENDOR_CLIENT_ID to something else. For example a cached class method.
def self.vendor_client_id
  @_vendor_client_id ||= Rails.application.credentials.vendor_api[:client_id].freeze
end