Last active
April 4, 2024 18:35
-
-
Save ToTenMilan/576d92c56ceb676602157a3d29f5c1e0 to your computer and use it in GitHub Desktop.
Test stripe webhook signature outside of controller context
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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: '[email protected]') } | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 errorWebhook signature verification failed
Note that I am using STRIPE_MODE to switch between test mode and live mode webhook secrets.