Back to Repositories

Testing Webhook Request Processing Implementation in DocuSeal

This test suite validates the webhook functionality for form completion events in DocuSeal, ensuring proper handling of webhook requests and retries. It covers the core webhook delivery system and various authentication scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the SendFormCompletedWebhookRequestJob functionality.

Key areas tested include:
  • Basic webhook request sending with proper headers and payload
  • Secret header authentication handling
  • Event type filtering
  • Retry mechanism for failed requests
  • Maximum retry attempt limitations

Implementation Analysis

The testing approach uses RSpec’s behavior-driven development pattern with extensive use of let blocks for test setup. The implementation leverages WebMock for HTTP request stubbing and validates both successful and error scenarios with precise request matching.

Notable patterns include:
  • Factory-based test data setup
  • Stub-based HTTP interaction testing
  • Job queue validation

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • WebMock for HTTP request stubbing
  • FactoryBot for test data generation
  • Rails test helpers for job queue inspection
  • Custom certificate generation for E-sign functionality

Best Practices Demonstrated

The test suite exemplifies several testing best practices in Ruby and RSpec.

Notable practices include:
  • Isolated test scenarios with proper setup and teardown
  • Comprehensive edge case coverage
  • Clear test organization and naming
  • Effective use of RSpec’s expect syntax
  • Proper separation of concerns in test setup

docusealco/docuseal

spec/jobs/send_form_completed_webhook_request_job_spec.rb

            
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SendFormCompletedWebhookRequestJob do
  let(:account) { create(:account) }
  let(:user) { create(:user, account:) }
  let(:template) { create(:template, account:, author: user) }
  let(:submission) { create(:submission, template:, created_by_user: user) }
  let(:submitter) do
    create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
  end
  let(:webhook_url) { create(:webhook_url, account:, events: ['form.completed']) }

  before do
    create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
                              value: GenerateCertificate.call.transform_values(&:to_pem))
  end

  describe '#perform' do
    before do
      stub_request(:post, webhook_url.url).to_return(status: 200)
    end

    it 'sends a webhook request' do
      described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)

      expect(WebMock).to have_requested(:post, webhook_url.url).with(
        body: {
          'event_type' => 'form.completed',
          'timestamp' => /.*/,
          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
        },
        headers: {
          'Content-Type' => 'application/json',
          'User-Agent' => 'DocuSeal.com Webhook'
        }
      ).once
    end

    it 'sends a webhook request with the secret' do
      webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' })
      described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)

      expect(WebMock).to have_requested(:post, webhook_url.url).with(
        body: {
          'event_type' => 'form.completed',
          'timestamp' => /.*/,
          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
        },
        headers: {
          'Content-Type' => 'application/json',
          'User-Agent' => 'DocuSeal.com Webhook',
          'X-Secret-Header' => 'secret_value'
        }
      ).once
    end

    it "doesn't send a webhook request if the event is not in the webhook's events" do
      webhook_url.update!(events: ['form.declined'])

      described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)

      expect(WebMock).not_to have_requested(:post, webhook_url.url)
    end

    it 'sends again if the response status is 400 or higher' do
      stub_request(:post, webhook_url.url).to_return(status: 401)

      expect do
        described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
      end.to change(described_class.jobs, :size).by(1)

      expect(WebMock).to have_requested(:post, webhook_url.url).once

      args = described_class.jobs.last['args'].first

      expect(args['attempt']).to eq(1)
      expect(args['last_status']).to eq(401)
      expect(args['webhook_url_id']).to eq(webhook_url.id)
      expect(args['submitter_id']).to eq(submitter.id)
    end

    it "doesn't send again if the max attempts is reached" do
      stub_request(:post, webhook_url.url).to_return(status: 401)

      expect do
        described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11)
      end.not_to change(described_class.jobs, :size)

      expect(WebMock).to have_requested(:post, webhook_url.url).once
    end
  end
end