As I was looking to sure up some billing code in one of my projects, I figured it was finally time that I created some tests for the various Stripe webhooks that we used.
I was a bit dishearted that the only webhook testing examples I could find online pertained to
RSpec, so with that I rolled up my sleaves and dug into the internals of both the stripe_event
gem
as well as Stripe’s offical gem.
After a few rounds of trial and error I was able to discern how Stripe generates the signature
for webhooks and how I could mimic it to test my endpoint. You can see that below in the
webhook_header
method.
You’ll also need to grab sample payloads for each of the webhook events you’d like to test and save
them as a JSON file in test/fixtures/stripe/#{event}.json
, as seen in the webhook_payload
method.
If you aren’t sure where to grab sample payloads from,
the Stripe CLI has a trigger
command that can make
gathering them much easier.
The only other varaible you may have is the path to your webhook endpoint, which in my
case is stripe_event_path
, as seen in the deliver_payload
method.
Take all of that and you get the following test case can be used as a base class for any tests you may need to write:
# test/stripe_test_case.rb
require "test_helper"
class StripeWebhookTestCase < ActionDispatch::IntegrationTest
SIGNING_SECRET = ENV["STRIPE_SIGNING_SECRET"]
def deliver_payload(event:)
params, headers = webhook_setup(event)
post stripe_event_path, params:, headers:
end
def webhook_setup(event)
payload = webhook_payload(event)
[payload, webhook_header(payload:)]
end
def webhook_payload(event)
File.read(Rails.root.join("test", "fixtures", "stripe", "#{event}.json"))
end
def webhook_header(payload:)
time = Time.now
signature = Stripe::Webhook::Signature.compute_signature(time, payload, SIGNING_SECRET)
header = Stripe::Webhook::Signature.generate_header(time, signature)
{"Stripe-Signature": header}
end
end
Once you have this test case, here’s an example test that could be used:
# test/webhooks/stripe/general_webhook_test.rb
require "stripe_webhook_test_case"
class Stripe::GeneralWebhookTest < StripeWebhookTestCase
test "webhook endpoint returns proper status code" do
deliver_payload(event: "charge.succeeded")
assert_response :ok
end
test "webhook endpoint returns proper status code when no headers are provided" do
post stripe_event_path, params: webhook_payload("charge.succeeded")
assert_response :bad_request
end
test "webhook endpoint returns proper status code when improper headers are provided" do
post stripe_event_path, params: webhook_payload("charge.succeeded"),
headers: {"Stripe-Signature": "#{Time.now.to_i},v1=badsignature"}
assert_response :bad_request
end
end
In this project I also happen to be using the stripe_event
gem,
so a good deal of the actual route/endpoint logic is abstracted from my app. I’m currently considering rolling
my own completely and removing this gem as a dependancy, as I’ve done in another project already, but
that’s a topic for another post. The Stripe docs also have
a really good example one could use as a starting point.