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.
Related
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
I'm doing a stub request to File but since I call Tempfile (that is a subclass of File) before calling File, Tempfile is intercepting the stub I define.
Model:
def download_file
#...
begin
tempfile = Tempfile.new([upload_file_name, '.csv'])
File.open(tempfile, 'wb') { |f| f.write(result.body.to_s) }
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
end
Rspec example:
it 'could not write to tempfile, so the report is created but without content' do
allow(File).to receive(:open).and_return Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
Error:
tempfile in the line
tempfile = Tempfile.new([upload_file_name, '.csv'])
receives Errno::ENOENT, indicating that the stub is going to Tempfile instead of to File.
How can I define the stub to go to File instead of to Tempfile?
There's no need to reopen a Tempfile, it's already open and delegates to File.
def download_file
tempfile = Tempfile.new([upload_file_name, '.csv'])
tempfile.write(result.body.to_s)
tempfile.path
# A method has an implicit begin.
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Then you can mock just Tempfile.new. Note that exceptions are raised, not returned.
it 'could not write to tempfile, so the report is created but without content' do
# Exceptions are raised, not returned.
allow(Tempfile).to receive(:new)
.and_raise Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
However, this remains fragile glass-box testing. Your test has knowledge of the implementation, if the implementation changes the test gives a false negative. And it still has to hope mocking Tempfile.new doesn't break something else.
Instead, extract temp file creation from download_file.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Now the mocking can be targeted to a specific method in a specific object. And we can apply some good rspec patterns.
context 'when the Tempfile cannot be created' do
# Here I'm assuming download_file is part of the Controller being tested.
before do
allow(#controller).to receive(:new_temp_file_for_upload)
.and_raise Errno::ENOENT
end
it 'creates the report without content' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:success)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to be nil
end
end
Note: returning "success" and an empty report after an internal failure is probably incorrect. It should return a 5xx error so the user knows there was a failure without having to look at the content.
download_file is doing too many things. It's both downloading a file and deciding what to do with a specific error. It should just download the file. Let something higher up in the call stack decide what to do with the exception. Methods which do one thing are simpler and more flexible and easier to test and less buggy.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
end
context 'when the download fails' do
before do
allow(#controller).to receive(:download_file)
.and_raise "krunch!"
end
it 'responds with an error' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:error)
end
end
Note that no specific error is needed. It's enough that download_file raises an exception. This test now has no knowledge of the internals beyond knowing that download_file is called.
I have the following code in my API file
class TransactionStatus < Grape::API
helpers ::PushAPIv1::NamedParams
post '/transaction/status' do
Rails.logger.warn "#{params.to_xml}"
// some piece of code
end
end
I tried to write Rpsec for this condition but not getting any success in my coverage report. The Spec I tried to write is as below
require 'rails_helper'
RSpec.describe Transaction, type: :request do
context 'check Transaction Status' do
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
p transaction.ref_code
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
end
end
Well, if what you're trying to achieve is coverage of your POST /transaction/status endpoint, then... you need to reach for the endpoint in your specs, which you're not doing at the moment.
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
Here you expect Rails.logger to receive warn message. But you need to trigger something that should call Rails.logger.warn for the spec to pass.
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
About this spec: you're mixing expect and should syntaxes in a manner that's hardly understandable. Plus you're just using ActiveRecord methods, and never call your actual API endpoint. That's why you don't get any code coverage.
In the end, what you should do to get proper coverage of your endpoint is actually calling it. This could be done in a before :each block for instance, or even in each of your spec, like this:
describe 'transaction/status' do
before :each do
post 'path/to/api/transaction/status'
# post 'path/to/api/transaction/status', params: { some: params }
# post 'path/to/api/transaction/status', headers: { some: headers }
end
it '...' do
expect( response ).to ...
end
end
You get the idea. You can check out RSpec Rails 3.7 - Request specs for more details and examples.
I'm trying to write tests for an API which requires an hmac signature on each request.
describe Api::V2::HmacController, :type => :controller do
render_views
it 'GET' do
get :index, timestamp: Time.now.to_i, format: :json
expect(response.status).to eq(200)
end
end
I would like to add
request.env['x-api-key'] = API_KEY
request.env['x-api-hmac'] = "Encode"(API_SECRET, "parameters of the request")
to each request.
I'm open to any type of solution.
Question: How can I wedge a hook after the request has been formed, but hasn't sent?
I'm thinking of overwriting rspec get / post method, but I'm not sure how.
You can do it from in the block
request.headers['x-api-key'] = API_KEY
You shouldn't set access the request headers through the env.
See here
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