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.
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 saw an error that can occur when you press a button too many times and I dealt with it with a rescue, so the controller is:
def all_list
filename = "#{current_organization.name.gsub(' ', '_')}.zip"
temp_file = Tempfile.new(filename)
begin
#things to create a zip
rescue Errno::ENOENT
puts 'Something went wrong'
redirect_to all_list_report_path
ensure
temp_file.close
temp_file.unlink
end
end
And I tried a lot of things with rspec to test this rescue block, my last try was:
context 'with a logged user that try to' do
before do
login(member1.user)
allow(ReportsController).to receive(:all_list).and_raise(Errno::ENOENT)
end
it 'throws the error but downloads the zip' do
get :all_list
expect(ReportsController).to receive(:all_list).and_raise(Errno::ENOENT)
end
end
It seems to work, but the block rescue is not covered and I tried to see if the block is called with puts 'Something went wrong' but obviusly does not print anything.
I'm looking for a way to cover the block effectively. Any help is welcome.
You have to raise the error for method which are called inside the begin block. Raising error for all_list method will never execute begin and rescue block.
def all_list
filename = "#{current_organization.name.gsub(' ', '_')}.zip"
temp_file = Tempfile.new(filename)
begin
things_to_create_a_zip
rescue Errno::ENOENT
puts 'Something went wrong'
redirect_to all_list_report_path
ensure
temp_file.close
temp_file.unlink
end
end
Allowing things_to_create_a_zip method to raise the error Errno::ENOENT, will executes the rescue block.
context 'with a logged user that try to' do
before do
login(member1.user)
allow(subject).to receive(:things_to_create_a_zip).and_raise(Errno::ENOENT)
end
it 'redirect to all_list_report_path' do
get :all_list
expect(response).to redirect_to(all_list_report_path)
end
end
I have method like this
def className
def method_name
some code
rescue
some code and error message
end
end
So, How to write down the rspec to test rescue block..?
If you want to rescue, it means you expect some code to raise some kind of exception.
You can use RSpec stubs to fake the implementation and force an error. Assuming the execution block contains a method that may raise
def method_name
other_method_that_may_raise
rescue => e
"ERROR: #{e.message}"
end
hook the stub to that method in your specs
it " ... " do
subject.stub(:other_method_that_may_raise) { raise "boom" }
expect { subject.method_name }.to_not raise_error
end
You can also check the rescue handler by testing the result
it " ... " do
subject.stub(:other_method_that_may_raise) { raise "boom" }
expect(subject.method_name).to eq("ERROR: boom")
end
Needless to say, you should raise an error that it's likely to be raised by the real implementation instead of a generic error
{ raise FooError, "boom" }
and rescue only that Error, assuming this is relevant.
As a side note, in Ruby you define a class with:
class ClassName
not
def className
as in your example.
you can stub with return error
for example you have class with method like this :
class Email
def self.send_email
# send email
rescue
'Error sent email'
end
end
so rspec for raising error is
context 'when error occures' do
it 'should return error message' do
allow(Email).to receive(:send_email) { err }
expect(Email.send_email).to eq 'Error sent email brand'
end
end
I'm working the a Documents class, trying to test it. I've defined the following factory:
require 'factory_girl'
FactoryGirl.define do
factory :document do
user_id '6315'
name 'Test doc'
description 'W9'
filename 'test_doc.pdf'
filetype 'file'
filesize 500
end
factory :invalid_doc, parent: :document do
filesize 5242900
end
end
with the following helper method to access the right attributes in the test:
def build_attributes(*args)
attrs = FactoryGirl.build(*args).attributes
attrs.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
paramify_values(attrs)
end
Before each test I run:
before(:each) do
login_as_admin
#doc = #user.documents.create(FactoryGirl.attributes_for(:document))
end
where #user is set in the login_as_admin macro. Within my test, I'm running this:
describe 'POST #create' do
it "should create a new document" do
expect{
post :create, document: build_attributes(:document, user_id: #doc.user_id)
}.to change(Document,:count).by(1)
end
it "should find the right user" do
post :create, document: build_attributes(:document, user_id: #doc.user_id)
assigns(:user).should eq(#user)
end
# some other tests...
end
The former test was suggested on this article, the latter is just what I think should be happening. The controller action is assigning the instance with the following:
#user = User.find(document[:user_id])
so, pretty standard. However, both of these tests throw the same error,
Failure/Error: post :create, document: build_attributes(:document, user_id: #doc.user_id)
NoMethodError:
undefined method `original_filename' for nil:NilClass
but I never call that method explicitly, so is it something FactoryGirl is calling? The model is described as follows:
attr_accessible :description, :filename, :filesize, :filetype, :name, :user_id
where :filename is just a string. What could be going wrong here? I'm not using paperclip to upload the files, just a file_field in the view. I grab the path and save the file to the production server in the controller, but never call this method.
Edit:
I suppose an actual controller description might help haha
def create
uploaded_file = params[:document][:file]
document = params[:document]
document.delete(:file)
#user = User.find(document[:user_id])
filepath = Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s, uploaded_file.original_filename)
%x[ mkdir #{Rails.root.join('documents', #user.company_id.to_s)} ]
%x[ mkdir #{Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s)} ]
File.open(filepath, 'wb') do |file|
file.write(uploaded_file.read)
end
document[:filesize]= File.size(filepath)
document[:filetype]= File.ftype(filepath)
document[:filename] = uploaded_file.original_filename
d =Document.new(document)
d.save
redirect_to :action => 'show', :id => user.id
end
Please keep in mind I'm sure there are many things wrong with this method. I'm trying to refactor it and test as I go. For the moment, all I'm trying to do is get past this first hiccough, the original_filename method is being called somewhere, and I don't define it myself. Can anyone see why/where?
original_filename is a method on an uploaded file, see the rack documentation.
filepath = Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s, uploaded_file.original_filename)
and
document[:filename] = uploaded_file.original_filename
In the controller are getting the original filename, since when a file gets uploaded it gets an ugly temp filename for storage you want to use the original filename to make it readable and accurate.
Consider using the fixture_file_upload helper in rspec. Here is an example spec:
expect {
post :create, document: attributes_for(:document, user_id: #doc.user_id, file: fixture_file_upload('spec/assets/documents/test_doc.pdf', 'appliation/pdf'))
}.to change(Document, :count).by(1)
And place a test pdf in spec/assets/documents/test_doc.pdf
You can use
Rack::Multipart::UploadedFile.new(path)
for your test.
I am trying to find the best way of writing an rspec test that will spec the call to
mail(mail_content).deliver
and raise an exception so I can assert the Rails.logger is called.
I know you are not meant to mock the class under test but does this apply to super classes?
class MyMailer < ActionMailer::Base
default from: 'test#test.com'
default charset: 'UTF-8'
default content_type: 'text/plain'
default subject: 'Test'
def send_email (to, mail_headers_hash, mail_body)
begin
headers mail_headers_hash
mail_content = {to: to, body: mail_body}
mail(mail_content).deliver
rescue ArgumentError, StandardError => e
Rails.logger.error "Failed to email order response - #{e}"
end
end
end
end
With RSpec you could stub raising exception. See the following code snippet:
whatever.should_receive(:do_some_stuff).and_raise(ArgumentError)