Webhook Simulation

FakeDataDSL's Webhook Simulator lets you send realistic webhook payloads to your application, simulating events from external services like Stripe, GitHub, Twilio, and more.

Quick Start

# Configure the simulator
FakeDataDSL::WebhookSimulator.configure do |config|
  config.endpoint = "http://localhost:3000/webhooks"
  config.secret = "whsec_your_webhook_secret"
end

# Send a webhook
FakeDataDSL::WebhookSimulator.send(
  "stripe.payment_intent.succeeded",
  schema: "StripePaymentIntent"
)

Basic Usage

Simple Webhook

# Send webhook with generated data
FakeDataDSL::WebhookSimulator.send(
  "user.created",
  schema: "User"
)

# With custom payload overrides
FakeDataDSL::WebhookSimulator.send(
  "order.completed",
  schema: "Order",
  data: { status: "completed", total: 99.99 }
)

With Delay

# Simulate network latency
FakeDataDSL::WebhookSimulator.send(
  "payment.failed",
  schema: "PaymentEvent",
  delay: 2.seconds
)

Batch Webhooks

# Send multiple webhooks in sequence
FakeDataDSL::WebhookSimulator.send_batch([
  { event: "order.created", schema: "Order" },
  { event: "payment.processing", schema: "Payment", delay: 1.second },
  { event: "payment.succeeded", schema: "Payment", delay: 2.seconds },
  { event: "order.completed", schema: "Order", delay: 3.seconds }
])

Configuration

Global Configuration

FakeDataDSL::WebhookSimulator.configure do |config|
  # Target endpoint
  config.endpoint = "http://localhost:3000/webhooks"

  # Webhook secret for signatures
  config.secret = ENV["WEBHOOK_SECRET"]

  # Default headers
  config.headers = {
    "Content-Type" => "application/json",
    "User-Agent" => "FakeDataDSL-WebhookSimulator/1.0"
  }

  # Timeout settings
  config.timeout = 30
  config.open_timeout = 5

  # Retry settings
  config.max_retries = 3
  config.retry_delay = 1.second

  # Logging
  config.logger = Rails.logger
  config.log_level = :info
end

Per-Request Configuration

FakeDataDSL::WebhookSimulator.send(
  "event.name",
  schema: "Schema",
  config: {
    endpoint: "http://different-endpoint.com/hooks",
    secret: "different_secret",
    timeout: 60
  }
)

Webhook Signatures

Automatic Signing

Webhooks are automatically signed based on the provider type:

# Stripe-style signature (HMAC-SHA256)
FakeDataDSL::WebhookSimulator.send(
  "payment_intent.succeeded",
  schema: "StripePayment",
  signature_type: :stripe
)
# Adds: Stripe-Signature: t=1234567890,v1=abc123...

# GitHub-style signature
FakeDataDSL::WebhookSimulator.send(
  "push",
  schema: "GitHubPush",
  signature_type: :github
)
# Adds: X-Hub-Signature-256: sha256=abc123...

# Custom signature
FakeDataDSL::WebhookSimulator.send(
  "custom.event",
  schema: "Custom",
  signature: ->(payload, secret) {
    OpenSSL::HMAC.hexdigest("SHA512", secret, payload)
  },
  signature_header: "X-Custom-Signature"
)

Supported Signature Types

Provider Type Header
Stripe :stripe Stripe-Signature
GitHub :github X-Hub-Signature-256
Twilio :twilio X-Twilio-Signature
Shopify :shopify X-Shopify-Hmac-SHA256
Custom :custom Configurable

Provider Presets

Stripe

FakeDataDSL::WebhookSimulator.stripe(
  "payment_intent.succeeded",
  data: {
    amount: 2000,
    currency: "usd",
    customer: "cus_123"
  }
)

# Uses built-in Stripe event schemas
# Automatically formats as Stripe webhook structure:
# {
#   "id": "evt_...",
#   "object": "event",
#   "type": "payment_intent.succeeded",
#   "data": { "object": { ... } }
# }

GitHub

FakeDataDSL::WebhookSimulator.github(
  "push",
  repository: "owner/repo",
  data: {
    ref: "refs/heads/main",
    commits: [{ message: "Update README" }]
  }
)

Twilio

FakeDataDSL::WebhookSimulator.twilio(
  "message.received",
  data: {
    from: "+15551234567",
    body: "Hello, world!"
  }
)

Custom Provider

FakeDataDSL::WebhookSimulator.register_provider(:my_service) do |config|
  config.base_url = "https://my-service.com"
  config.signature_type = :hmac_sha256
  config.signature_header = "X-MyService-Signature"
  config.event_wrapper = ->(event, data) {
    {
      event_type: event,
      timestamp: Time.current.iso8601,
      payload: data
    }
  }
end

# Use registered provider
FakeDataDSL::WebhookSimulator.my_service(
  "user.updated",
  schema: "User"
)

Event Sequences

Define Sequences

# Define a sequence of related events
FakeDataDSL::WebhookSimulator.define_sequence(:checkout_flow) do
  # Step 1: Cart created
  step "cart.created", schema: "Cart"

  # Step 2: Payment initiated (after 1 second)
  step "payment.initiated", schema: "Payment", delay: 1.second

  # Step 3: Payment succeeded (after 2 more seconds)
  step "payment.succeeded", schema: "Payment", delay: 2.seconds

  # Step 4: Order created
  step "order.created", schema: "Order", delay: 500.milliseconds

  # Step 5: Email sent
  step "notification.sent", schema: "Notification", delay: 1.second
end

Run Sequences

# Run the sequence
FakeDataDSL::WebhookSimulator.run_sequence(:checkout_flow)

# Run with shared context (data passed between steps)
FakeDataDSL::WebhookSimulator.run_sequence(:checkout_flow,
  context: { user_id: "user_123", amount: 99.99 }
)

# Run with callbacks
FakeDataDSL::WebhookSimulator.run_sequence(:checkout_flow) do |step, response|
  puts "Step #{step.name}: #{response.status}"
end

Conditional Steps

FakeDataDSL::WebhookSimulator.define_sequence(:payment_flow) do
  step "payment.initiated", schema: "Payment"

  # Branch based on previous response
  branch do |context|
    if context[:payment_method] == "card"
      step "payment.card.processing", schema: "CardPayment"
    else
      step "payment.bank.processing", schema: "BankPayment"
    end
  end

  step "payment.completed", schema: "Payment"
end

Testing Integration

RSpec Helpers

# spec/rails_helper.rb
require 'fake_data_dsl/webhook_simulator/rspec'

RSpec.configure do |config|
  config.include FakeDataDSL::WebhookSimulator::RSpecHelpers
end
# spec/webhooks/stripe_spec.rb
RSpec.describe "Stripe Webhooks", type: :request do
  it "handles payment_intent.succeeded" do
    simulate_webhook "stripe.payment_intent.succeeded",
      schema: "StripePayment",
      data: { amount: 5000 }

    expect(response).to have_http_status(:ok)
    expect(Payment.last.amount).to eq(50.00)
  end

  it "handles the full checkout sequence" do
    simulate_webhook_sequence :checkout_flow

    expect(Order.count).to eq(1)
    expect(Order.last.status).to eq("completed")
  end
end

Minitest Helpers

# test/test_helper.rb
require 'fake_data_dsl/webhook_simulator/minitest'

class ActiveSupport::TestCase
  include FakeDataDSL::WebhookSimulator::MinitestHelpers
end
# test/integration/webhooks_test.rb
class WebhooksTest < ActionDispatch::IntegrationTest
  test "handles stripe webhook" do
    simulate_webhook "stripe.payment_intent.succeeded",
      schema: "StripePayment"

    assert_response :success
  end
end

Error Simulation

Simulate Failures

# Simulate webhook delivery failure
FakeDataDSL::WebhookSimulator.send(
  "event.name",
  schema: "Schema",
  simulate_failure: true  # Returns 500 error simulation
)

# Simulate timeout
FakeDataDSL::WebhookSimulator.send(
  "event.name",
  schema: "Schema",
  simulate_timeout: true
)

# Simulate partial failure (intermittent)
FakeDataDSL::WebhookSimulator.send(
  "event.name",
  schema: "Schema",
  failure_rate: 0.3  # 30% chance of failure
)

Retry Testing

# Test retry behavior
FakeDataDSL::WebhookSimulator.send(
  "event.name",
  schema: "Schema",
  fail_first_n: 2  # Fail first 2 attempts, succeed on 3rd
)

Recording and Replay

Record Webhooks

# Record all webhooks sent
FakeDataDSL::WebhookSimulator.start_recording

# ... send webhooks ...

# Stop and save
recordings = FakeDataDSL::WebhookSimulator.stop_recording
recordings.save_to("spec/fixtures/webhooks/checkout_flow.json")

Replay Recordings

# Replay recorded webhooks
FakeDataDSL::WebhookSimulator.replay("spec/fixtures/webhooks/checkout_flow.json")

# With timing
FakeDataDSL::WebhookSimulator.replay("recordings.json",
  preserve_timing: true  # Replay with original delays
)

CLI Commands

# Send single webhook
fake_data_dsl webhook send user.created \
  --schema User \
  --endpoint http://localhost:3000/webhooks \
  --secret whsec_xxx

# Send with data override
fake_data_dsl webhook send payment.succeeded \
  --schema Payment \
  --data '{"amount": 9999}' \
  --endpoint http://localhost:3000/webhooks

# Run sequence
fake_data_dsl webhook sequence checkout_flow \
  --endpoint http://localhost:3000/webhooks

# Record mode
fake_data_dsl webhook record \
  --output recordings/session.json \
  --duration 60  # Record for 60 seconds

# Replay recordings
fake_data_dsl webhook replay recordings/session.json \
  --endpoint http://localhost:3000/webhooks

API Reference

WebhookSimulator.send

FakeDataDSL::WebhookSimulator.send(event_name, options = {})

Parameters:

  • event_name - Event type string (e.g., "user.created")
  • options[:schema] - Schema name for payload generation
  • options[:data] - Override data
  • options[:delay] - Delay before sending
  • options[:endpoint] - Override endpoint
  • options[:secret] - Override secret
  • options[:signature_type] - Signature type
  • options[:headers] - Additional headers
  • options[:config] - Full config override

Returns: WebhookResponse object

WebhookSimulator.send_batch

FakeDataDSL::WebhookSimulator.send_batch(webhooks)

Parameters:

  • webhooks - Array of webhook configurations

Returns: Array of WebhookResponse objects

WebhookSimulator.define_sequence

FakeDataDSL::WebhookSimulator.define_sequence(name, &block)

Parameters:

  • name - Sequence name
  • block - Sequence definition block

WebhookSimulator.run_sequence

FakeDataDSL::WebhookSimulator.run_sequence(name, options = {}, &callback)

Parameters:

  • name - Sequence name
  • options[:context] - Shared context data
  • callback - Optional callback for each step

Returns: Array of WebhookResponse objects

Best Practices

1. Use Consistent Secrets

# config/environments/test.rb
ENV["WEBHOOK_SECRET"] = "test_webhook_secret"

# spec/rails_helper.rb
FakeDataDSL::WebhookSimulator.configure do |config|
  config.secret = ENV["WEBHOOK_SECRET"]
end

2. Test Error Handling

it "handles webhook failures gracefully" do
  simulate_webhook "payment.failed",
    schema: "FailedPayment",
    data: { error_code: "card_declined" }

  expect(response).to have_http_status(:ok)
  expect(Payment.last.status).to eq("failed")
  expect(User.last).to have_received_email(:payment_failed)
end

3. Use Sequences for Complex Flows

# Better than individual webhook tests
simulate_webhook_sequence :full_order_lifecycle

# Instead of:
simulate_webhook "order.created"
simulate_webhook "payment.succeeded"
simulate_webhook "order.shipped"
simulate_webhook "order.delivered"

4. Record Production Webhooks for Testing

# In development, record real webhooks
# Then replay in tests for realistic scenarios

Troubleshooting

Signature Verification Failing

# Make sure secrets match
FakeDataDSL::WebhookSimulator.configure do |config|
  config.secret = ENV["WEBHOOK_SECRET"]  # Same as your app uses
end

# Debug signature
response = FakeDataDSL::WebhookSimulator.send(...)
puts response.request_headers["Stripe-Signature"]

Timeout Issues

# Increase timeout
FakeDataDSL::WebhookSimulator.configure do |config|
  config.timeout = 60
  config.open_timeout = 10
end

Endpoint Not Reachable

# Make sure your server is running
# In tests, use request specs not system specs
RSpec.describe "Webhooks", type: :request do
  # This runs against the test server
end

See Also