Back to Repositories

Testing Form Started Webhook Request Processing in DocuSeal

This test suite validates the webhook notification system for form start events in DocuSeal, ensuring proper handling of webhook requests and retries. The tests verify the webhook delivery mechanism, header configurations, and error handling scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the SendFormStartedWebhookRequestJob functionality, including:

  • Basic webhook request sending with proper event data
  • Custom header handling with secrets
  • Event filtering validation
  • Retry mechanism for failed requests
  • Maximum retry attempt enforcement

Implementation Analysis

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

The tests demonstrate proper isolation through factory usage and mock responses.

Technical Details

Key technical components include:

  • RSpec testing framework
  • WebMock for HTTP request stubbing
  • Factory setup for test data generation
  • Rails test helpers and configurations
  • JSON response validation
  • Webhook URL and encrypted configuration handling

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test scenarios with proper setup and teardown
  • Comprehensive edge case coverage
  • Clear test descriptions and organization
  • Proper mocking of external services
  • Validation of both successful and error paths
  • Efficient test data setup using factories

docusealco/docuseal

spec/jobs/send_form_started_webhook_request_job_spec.rb

            
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SendFormStartedWebhookRequestJob 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.started']) }

  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.started',
          '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.started',
          '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