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

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