Back to Repositories

Testing Form Decline Webhook Request Processing in DocuSeal

This test suite validates the SendFormDeclinedWebhookRequestJob functionality in DocuSeal, focusing on webhook request handling for declined form submissions. The tests ensure proper webhook delivery, header management, and retry logic for failed requests.

Test Coverage Overview

The test suite provides comprehensive coverage of webhook request handling for declined forms. Key areas tested include:

  • Basic webhook request sending with proper event data
  • Custom header handling with secrets
  • Event filtering based on webhook configuration
  • Retry logic for failed requests
  • Maximum attempt limitation checks

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns 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.

The tests employ factory-based test data generation and mock external service interactions effectively.

Technical Details

Testing tools and configuration:

  • RSpec as the testing framework
  • WebMock for HTTP request stubbing
  • Factory-based fixtures for test data
  • Rails test helpers for database interaction
  • Encrypted configuration handling for certificates

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 organization and naming
  • Effective use of shared contexts and helper methods
  • Proper assertion of both positive and negative scenarios

docusealco/docuseal

spec/jobs/send_form_declined_webhook_request_job_spec.rb

            
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SendFormDeclinedWebhookRequestJob 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.declined']) }

  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.declined',
          '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.declined',
          '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.completed'])

      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