Skip to content

Instantly share code, notes, and snippets.

@ToTenMilan
Last active April 4, 2024 18:35
Show Gist options
  • Save ToTenMilan/576d92c56ceb676602157a3d29f5c1e0 to your computer and use it in GitHub Desktop.
Save ToTenMilan/576d92c56ceb676602157a3d29f5c1e0 to your computer and use it in GitHub Desktop.
Test stripe webhook signature outside of controller context
# Controlller
class Webhooks::StripeController < ApplicationController
skip_before_action :verify_authenticity_token
def index
stripe_service = StripeService.new(
request.env['HTTP_STRIPE_SIGNATURE'],
request.body.read
)
render json: { message: stripe_service.message }, status: stripe_service.status
end
end
# Service class
class StripeService
attr_reader :message, :status
def initialize(signature, request_body)
@request_body = request_body
@signature = signature
@event = nil
construct_event
end
private
def construct_event
endpoint_secret = ENV['STRIPE_MODE'] == 'live_mode' || Rails.env.test? ?
ENV['WEBHOOK_SECRET_STRIPE_LIVE_MODE'] : ##### set live webhook secret for test environment
ENV['WEBHOOK_SECRET_STRIPE_TEST_MODE']
# verify_payload
begin
@event = Stripe::Event.construct_from(JSON.parse(@request_body, symbolize_names: true))
@event = Stripe::Webhook.construct_event(@request_body, @signature, endpoint_secret)
rescue JSON::ParserError => e
@message = "⚠️ Webhook error while parsing basic request. #{e.message}"
@status = 400
return
rescue Stripe::SignatureVerificationError => e
@message = "⚠️ Webhook signature verification failed. #{e.message}"
@status = 400
return
end
case @event.type
when 'some.stripe.event'
# do your thing
else
puts "Unhandled event type: #{@event.type}"
end
@message = 'Success'
@status = 200
end
end
# Service spec
require 'rails_helper'
require 'openssl'
require 'base64'
describe StripeService do
let!(:user) { create(:user, email: 'foo@foo.com') }
context 'when a real webhook request is sent' do
it 'returns a success message' do
stripe_service = StripeService.new(
signature,
stripe_real_webhook_request
)
expect(stripe_service.message).to eq('Success')
expect(stripe_service.status).to eq(200)
# expect your objects have changed after handing event
end
end
context 'when an invalid signature is sent' do
it 'returns a failed message' do
stripe_service = StripeService.new(
'invalid_signature',
stripe_real_webhook_request
)
expect(stripe_service.message).to include('Webhook signature verification failed')
expect(stripe_service.status).to eq(400)
# expect your objects did not change after handing event
end
end
context 'when a payload is invalid' do
it 'returns a failed message' do
stripe_service = StripeService.new(
signature,
'invalid_payload'
)
expect(stripe_service.message).to include('Webhook error while parsing basic request')
expect(stripe_service.status).to eq(400)
# expect your objects did not change after handing event
end
end
end
def signature
timestamp = Time.new
stripe_signature = Stripe::Webhook::Signature.compute_signature(
timestamp,
stripe_real_webhook_request,
ENV['WEBHOOK_SECRET_STRIPE_LIVE_MODE']
)
"t=#{timestamp.to_i},v1=#{stripe_signature}"
end
def stripe_real_webhook_request
{
"id": "evt_absdeabcdefabcdefabcdef",
"object": "event",
"api_version": "2023-10-16",
"created": 1911556325,
"data": {
"object": {
# get the object shape from stripe docs
},
"type": "some.stripe.event"
}.to_json
end
# controller spec
require 'rails_helper'
RSpec.describe "Webhooks::StripeController", type: :request do
describe "POST path/to/stripe/webhooks" do
let(:stripe_service_mock) { instance_double(StripeService, message: 'Success', status: 200) }
before do
expect(StripeService).to receive(:new).and_return(stripe_service_mock)
end
it "returns http success" do
post "/path/to/stripe/webhooks"
expect(response).to have_http_status(:success)
end
end
end
@ToTenMilan
Copy link
Author

ToTenMilan commented Apr 4, 2024

I tested the stripe webhook signature outside of the controller context. One of the key parts is to pass real webhook secret in test environment to create the signature (just as stripe is creating it) that will be verified in tests. I am holding the real webhook secret under WEBHOOK_SECRET_STRIPE_LIVE_MODE. Without it, or with test mode webhook, I am getting the error Webhook signature verification failed

Note that I am using STRIPE_MODE to switch between test mode and live mode webhook secrets.

@ToTenMilan
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment