I'm using Oauth so what I do is store access_token and refresh token at User table, I create some classes to do this. In the Create class I do the normal functionality of the code (create records on the integration). The access_token expire at 1 hour, so intead of schedule an active job to refresh that token at that time I decided to do Refresh.new(user).call to request a new access_token and refresh_token.
I know that code works, because I've tested on live and I'm getting the new token when the access_token is expired. But I want to do a rspec test for this.
part of my rspec test:.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized).once
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
This is the response:
(RestClient).post(# data)
expected: 1 time with any arguments
received: 2 times with arguments: (# data)
This is my code:
Create.rb
class Create
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
create_contact
rescue RestClient::Unauthorized => ex
retry if Refresh.new(user).call
rescue RestClient::ExceptionWithResponse => ex
logger.error(#message.api_error(ex))
raise
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def create_contact
response = RestClient.post(
url, contact_params, contact_headers
)
logger.info(#message.api_response(response))
end
end
Refresh.rb
class Refresh
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
refresh_authorization_code
end
def refresh_authorization_code
response = RestClient.post(url, authorization_params)
logger.info(#message.api_response(response))
handle_response(response)
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def handle_response(response)
parsed = JSON.parse(response)
user.update!(access_token: parsed[:access_token], refresh_token: parsed[:refresh_token])
end
end
Also I tried using something like this from here
errors_to_raise = 2
allow(RestClient).to receive(:get) do
return rest_response if errors_to_raise <= 0
errors_to_raise -= 1
raise RestClient::Unauthorized
end
# ...
expect(client_response.code).to eq(200)
but I don't know how handle it propertly.
Your test calls RestClient.post twice, first in Create then again in Retry. But you only mocked one call. You need to mock both calls. The first call raises an exception, the second responds with a successful result.
We could do this by specifying an order with ordered...
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized)
.ordered
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.and_return(auth_response)
.ordered
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
However, this makes a lot of assumptions about exactly how the code works, and that nothing else calls RestClient.post.
More robust would be to use with to specify responses with specific arguments, and also verify the correct arguments are being passed.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_raise(RestClient::Unauthorized)
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_return(auth_response)
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
But this still makes a lot of assumptions about exactly how the code works, and you need to make a proper response.
Better would be to focus in on exactly what you're testing: when the create call gets an unauthorized exception it tries to refresh and does the call again. This unit test doesn't have to also test that Refresh#call works, just that Create#call calls it. You don't need to have RestClient.post raise an exception, just that Create#create_contact does.
context 'when token is expired' do
it 'requests a refresh token and retry' do
old_key = user.access_token
create = Create.new(user)
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It refreshes
refresh = double
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
# The refresh succeeds
expect(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
# It tries again
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
And you can also test when the retry fails. These can be combined together.
context 'when token is expired' do
let(:refresh) { double }
let(:create) { Create.new(user) }
before {
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It tries to refresh
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
}
context 'when the refresh succeeds' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
}
it 'retries' do
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
context 'when the refresh fails' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(false)
}
it 'does not retry' do
expect(create)
.not_to receive(:create_contact)
.ordered
create.call
end
end
end
Related
Stripe::Webhook.construct_event is raising Stripe::SignatureVerificationError when running request specs with rspec.
Using byebug Stripe::Webhook::Signature.verify_header is returning true. After continuing, the exception Stripe::SignatureVerificationError is raised.
From reviewing the source, it seems that the first call in Stripe::Webhook.construct_event is Stripe::Webhook::Signature.verify_header.
Why would the call in the debug console return true but apparently return false when it is called in .construct_event?
Webhook Controller
class WebHooks::StripeController < WebHooksController
# Entry point for Stripe webhooks. This method
# will verify the signature and dispatch to the
# appropriate method. It will log warning if
# the webhook type is unknown. The method dispatched is the
# webhook type with underscores instead of dots.
def create
payload = request.body.read
sig_header = request.headers['Stripe-Signature']
event = nil
byebug
# Byebug Console
Stripe::Webhook::Signature.verify_header(payload, sig_header, Rails.application.credentials.stripe[:signing_secret])
# => True, this returns true
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials.stripe[:signing_secret]
)
rescue JSON::ParserError => e
# Invalid payload
head :unprocessable_entity
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
Rails.logger.error("⚠️ Stripe signature verification failed.")
head :unauthorized
return
end
type = event.type.gsub('.', '_')
begin
public_send(type)
rescue NoMethodError
Rails.logger.warn("Unknown webhook type: #{params[:type]}")
head :unprocessable_entity
end
end
end
Spec
require 'rails_helper'
RSpec.describe "WebHooks::Stripe::Signature", type: :request do
context "with a valid signature" do
it "returns 200" do
event = { type: "not_implemented" }
headers = {
"Stripe-Signature" => stripe_event_signature(event.to_json)
}
post "/web_hooks/stripe", params: event.to_json, headers: headers
expect(response).to have_http_status(200) # This fails
end
end
context "an invalid signature" do
it "returns 401" do
post "/web_hooks/stripe", params: { type: "not_implemented" }
expect(response).to have_http_status(401)
end
end
end
Stripe Helper
module StripeTestHelper
def stripe_event_signature(payload)
time = Time.now
secret = Rails.application.credentials.stripe[:signing_secret]
signature = Stripe::Webhook::Signature.compute_signature(time, payload, secret)
Stripe::Webhook::Signature.generate_header(
time,
signature,
scheme: Stripe::Webhook::Signature::EXPECTED_SCHEME
)
end
end
There are a couple reasons as to why you might be getting this, Stripe::SignatureVerificationError error. The first being that a wrong value for your webhook signing secret, which would look something like whsec_, was used. You can retrieve the correct one from your Dashboard by clicking on ‘Reveal’.
If you've confirmed that you've used the right secret, then the issue might be on the payload's content which likely is the issue here. This answer on another SO talks about this in depth. The summary is that Rails for Ruby will tamper with the raw payload and if it's not identical to what Stripe sent you, the signature won't be a match.
As for next steps, you'd want try request.raw_post or find a similar solution to get to the exact raw JSON sent to you.
I have a SubscriptionHandler class with a call method that creates a pending subscription, attempts to bill the user and then error out if the billing fails. The pending subscription is created regardless of whether or not the billing fails
class SubscriptionHandler
def initialize(customer, stripe_token)
#customer = customer
#stripe_token = stripe_token
end
def call
create_pending_subscription
attempt_charge!
upgrade_subscription
end
private
attr_reader :stripe_token, :customer
def create_pending_subscription
#subscription = Subscription.create(pending: true, customer_id: customer.id)
end
def attempt_charge!
StripeCharger.new(stripe_token).charge! #raises FailedPaymentError
end
def upgrade_subscription
#subscription.update(pending: true)
end
end
Here is what my specs look like:
describe SubscriptionHandler do
describe "#call" do
it "creates a pending subscription" do
customer = create(:customer)
token = "token-xxx"
charger = StripeCharger.new(token)
allow(StripeCharger).to receive(:new).and_return(charger)
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
handler = SubscriptionHandler.new(customer, token)
expect { handler.call }.to change { Subscription.count }.by(1) # Fails with FailedPaymentError
end
end
end
But this does not change the subscription count, it fails with the FailedPaymentError. Is there a way to check that the subscription count increases without the spec blowing up with FailedPaymentError.
You should be able to use Rspec compound expectations for this
https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations
So I'll re-write your expectation to something like this:
expect { handler.call }.
to raise_error(FailedPaymentError).
and change { Subscription.count }.by(1)
It can be done like this
expect{ handler.call }.to raise_error FailedPaymentError
Should work.
If you don't want to raise error at all then you can remove this line, and return a valid response instead
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
More info - How to test exception raising in Rails/RSpec?
Official RSpec docs
https://relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/expect-error
My test "should call #logout_all" is failing with
expected: 1 time with any arguments
received: 0 times with any arguments
but when I call User.verify_from_token directly in the rails console, I can see that #logout_all is being called (I added a puts statement to #logout_all)
RSpec.describe User, type: :model do
describe ".verify_from_token" do
let(:user) {FactoryGirl.create(:user, verified: false)}
it "should return the user if found" do
token = user.to_valid_token
expect(User.verify_from_token token).to eq(user)
end
it "should verify the user" do
token = user.to_valid_token
User.verify_from_token token
expect(user.reload.verified).to eq(true)
end
it "should call #logout_all" do
token = user.to_valid_token
expect(user).to receive(:logout_all)
User.verify_from_token token
end
end
end
class User < ApplicationRecord
...
def self.verify_from_token token
user = from_token token
if user
user.update_attribute(:verified, true)
user.logout_all
user
else
nil
end
end
...
def logout_all
update_attribute(:token_timestamp, Time.now)
end
end
If I rework the test slightly, it works fine.
it "should call #logout_all" do
token = user.to_valid_token
t1 = user.token_timestamp
User.verify_from_token token
expect(t1 < user.reload.token_timestamp).to eq(true)
end
The problem is that you set expectation on one user, but method call happens on another user. This one.
user = from_token token
You didn't show implementation of from_token, but I'm willing to bet that it loads user from database. Now, there may be only one user in the database and, logically, these two variables represent the same entity. But physically, they're still two different objects in memory. So, naturally, the expectation is not met.
You have to do this:
it "should call #logout_all" do
token = user.to_valid_token
allow(User).to receive(:from_token).once.with(token).and_return(user)
expect(user).to receive(:logout_all)
User.verify_from_token token
end
Like it was said before, the method is called on a new User instance, not the one you set te expectation. You have to stub the "from_token" class method to return the same object you are expecting messages on, not another instance with the same id from the database.
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.
I'm using rspec request to test a JSON API that requires an api-key in the header of each request.
I know I can do this:
get "/v1/users/janedoe.json", {}, { 'HTTP_AUTHORIZATION'=>"Token token=\"mytoken\"" }
But it is tedious to do that for each request.
I've tried setting request.env in the before block, but I get the no method NilClass error since request doesn't exist.
I need some way, maybe in the spec-helper, to globally get this header sent with all requests.
To set it in a before hook you need to access it like
config.before(:each) do
controller.request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials('mytoken')
end
I too hated the giant hash, but preferred to be explicit in authorizing the user in different steps. After all, it's a pretty critical portion, and . So my solution was:
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
end
end
#spec/spec_helper.rb
RSpec.configure do |config|
...
config.include ControllerSpecHelpers, :type => :controller
then I can use it like so
describe Api::V1::Users, type: :controller do
it 'retrieves the user' do
user = create :user, name: "Jane Doe"
authorize user
get '/v1/users/janedoe.json'
end
end
I find this great for testing different authorization levels. Alternatively, you could have the helper method spec out the authorize function and get the same result, like so
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate
controller.stub(:authenticate! => true)
end
end
However, for ultimate speed and control, you can combine them
#spec/helpers/controller_spec_helpers.rb
module ControllerSpecHelpers
def authenticate user = nil
if user
token = Token.where(user_id: user.id).first || Factory.create(:token, user_id: user.id)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.hex)
else
controller.stub(:authenticate! => true)
end
end
end
and then authorize entire blocks with
#spec/spec_helper.rb
...
RSpec.configure do |config|
...
config.before(:each, auth: :skip) { authenticate }
#**/*_spec.rb
describe Api::V1::Users, type: :controller do
context 'authorized', auth: :skip do
...
I know that this question has already been answered but here's my take on it. Something which worked for me:
request.headers['Authorization'] = token
instead of:
request.env['Authorization'] = token
This is another way to do it if you are doing a post.
#authentication_params = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(Temp::Application.config.api_key) }
expect { post "/api/interactions", #interaction_params, #authentication_params }.to change(Interaction, :count).by(1)
Note interaction_params is just a json object I am passing in.
I don't think you should depend on the header if you are not testing the header itself, you should stub the method that checks if the HTTP_AUTORIZATION is present and make it return true for all specs except the spec that tests that particular header
something like...
on the controller
Controller...
before_filter :require_http_autorization_token
methods....
protected
def require_http_autorization_token
something
end
on the spec
before(:each) do
controller.stub!(:require_http_autorization_token => true)
end
describe 'GET user' do
it 'returns something' do
#call the action without the auth token
end
it 'requires an http_autorization_token' do
controller.unstub(:require_http_autorization_token)
#test that the actions require that token
end
end
that way one can forget the token and test what you really want to test