Back to Repositories

Testing Submissions API Endpoints in DocuSeal

This comprehensive test suite validates the Submissions API functionality in DocuSeal, covering CRUD operations, authentication, and error handling for document submissions. The tests ensure proper handling of multiple submitters, email validations, and template-specific constraints.

Test Coverage Overview

The test suite provides extensive coverage of the Submissions API endpoints including GET, POST, and DELETE operations. Key functionality tested includes:

  • Submission listing and retrieval
  • Creation of submissions with single and multiple submitters
  • Email-based submission creation
  • Submission archival
  • Authentication between production and test environments

Implementation Analysis

The testing approach utilizes RSpec request specs with factory-based test data generation. The implementation follows RESTful API testing patterns with detailed validation of response bodies, status codes, and error scenarios. The tests leverage RSpec’s let helpers for efficient test setup and reusable fixtures.

Technical Details

Testing tools and configuration:

  • RSpec for test framework
  • FactoryBot for test data generation
  • JSON response parsing and validation
  • Custom helper methods for response body verification
  • Authentication token handling

Best Practices Demonstrated

The test suite demonstrates several testing best practices including:

  • Comprehensive error case coverage
  • Isolation of test environments
  • Detailed response validation
  • DRY helper methods
  • Clear test organization and naming

docusealco/docuseal

spec/requests/submissions_spec.rb

            
# frozen_string_literal: true

require 'rails_helper'

describe 'Submission API', type: :request do
  let(:account) { create(:account, :with_testing_account) }
  let(:testing_account) { account.testing_accounts.first }
  let(:author) { create(:user, account:) }
  let(:testing_author) { create(:user, account: testing_account) }
  let(:folder) { create(:template_folder, account:) }
  let(:testing_folder) { create(:template_folder, account: testing_account) }
  let(:templates) { create_list(:template, 2, account:, author:, folder:) }
  let(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) }
  let(:testing_templates) do
    create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder)
  end

  describe 'GET /api/submissions' do
    it 'returns a list of submissions' do
      submissions = [
        create(:submission, :with_submitters,
               template: templates[0],
               created_by_user: author),
        create(:submission, :with_submitters,
               template: templates[1],
               created_by_user: author)
      ].reverse

      get '/api/submissions', headers: { 'x-auth-token': author.access_token.token }

      expect(response).to have_http_status(:ok)
      expect(response.parsed_body['pagination']).to eq(JSON.parse({
        count: submissions.size,
        next: submissions.last.id,
        prev: submissions.first.id
      }.to_json))
      expect(response.parsed_body['data']).to eq(JSON.parse(submissions.map { |t| index_submission_body(t) }.to_json))
    end
  end

  describe 'GET /api/submissions/:id' do
    it 'returns a submission' do
      submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author)

      get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }

      expect(response).to have_http_status(:ok)
      expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json))
    end

    it 'returns an authorization error if test account API token is used with a production submission' do
      submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author)

      get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': testing_author.access_token.token }

      expect(response).to have_http_status(:forbidden)
      expect(response.parsed_body).to eq(
        JSON.parse({ error: "Submission #{submission.id} not found using testing API key; " \
                            'Use production API key to access production submissions.' }.to_json)
      )
    end

    it 'returns an authorization error if production account API token is used with a test submission' do
      submission = create(:submission, :with_submitters, :with_events, template: testing_templates[0],
                                                                       created_by_user: testing_author)

      get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }

      expect(response).to have_http_status(:forbidden)
      expect(response.parsed_body).to eq(
        JSON.parse({ error: "Submission #{submission.id} not found using production API key; " \
                            'Use testing API key to access testing submissions.' }.to_json)
      )
    end
  end

  describe 'POST /api/submissions' do
    it 'creates a submission' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [{ role: 'First Party', email: '[email protected]' }]
      }.to_json

      expect(response).to have_http_status(:ok)

      submission = Submission.last

      expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
    end

    it 'creates a submission when the message is empty' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [{ role: 'First Party', email: '[email protected]' }],
        message: {}
      }.to_json

      expect(response).to have_http_status(:ok)

      submission = Submission.last

      expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
    end

    it 'creates a submission when some submitter roles are not provided' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: multiple_submitters_template.id,
        send_email: true,
        submitters: [
          { role: 'First Party', email: '[email protected]' },
          { email: '[email protected]' },
          { email: '[email protected]' }
        ]
      }.to_json

      expect(response).to have_http_status(:ok)

      submission = Submission.last

      expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
      expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json))
      expect(response.parsed_body[0]['role']).to eq('First Party')
      expect(response.parsed_body[0]['email']).to eq('[email protected]')
      expect(response.parsed_body[1]['role']).to eq('Second Party')
      expect(response.parsed_body[1]['email']).to eq('[email protected]')
      expect(response.parsed_body[2]['role']).to eq('Third Party')
      expect(response.parsed_body[2]['email']).to eq('[email protected]')
    end

    it 'returns an error if the submitter email is invalid' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [
          { role: 'First Party', email: 'john@example' }
        ]
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)

      expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' })
    end

    it 'returns an error if the template fields are missing' do
      templates[0].update(fields: [])

      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [{ role: 'First Party', email: '[email protected]' }]
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body).to eq({ 'error' => 'Template does not contain fields' })
    end

    it 'returns an error if submitter roles are not unique' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: multiple_submitters_template.id,
        send_email: true,
        submitters: [
          { role: 'First Party', email: '[email protected]' },
          { role: 'First Party', email: '[email protected]' }
        ]
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' })
    end

    it 'returns an error if number of submitters more than in the template' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [
          { email: '[email protected]' },
          { role: 'First Party', email: '[email protected]' }
        ]
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' })
    end

    it 'returns an error if the message has no body value' do
      post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        send_email: true,
        submitters: [
          { role: 'First Party', email: '[email protected]' }
        ],
        message: {
          subject: 'Custom Email Subject'
        }
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body).to eq({ 'error' => 'body is required in `message`.' })
    end
  end

  describe 'POST /api/submissions/emails' do
    it 'creates a submission using email' do
      post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        emails: '[email protected],[email protected]'
      }.to_json

      expect(response).to have_http_status(:ok)

      submissions = Submission.last(2)
      submissions_body = submissions.reduce([]) { |acc, submission| acc + create_submission_body(submission) }

      expect(response.parsed_body).to eq(JSON.parse(submissions_body.to_json))
    end

    it 'returns an error if emails are invalid' do
      post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: {
        template_id: templates[0].id,
        emails: '[email protected], [email protected]@gmail.com'
      }.to_json

      expect(response).to have_http_status(:unprocessable_entity)

      expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' })
    end
  end

  describe 'DELETE /api/submissions/:id' do
    it 'archives a submission' do
      submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author)

      delete "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token }

      expect(response).to have_http_status(:ok)

      submission.reload

      expect(submission.archived_at).not_to be_nil
      expect(response.parsed_body).to eq(JSON.parse({
        id: submission.id,
        archived_at: submission.archived_at
      }.to_json))
    end
  end

  private

  def index_submission_body(submission)
    submitters = submission.submitters.map do |submitter|
      {
        id: submitter.id,
        submission_id: submission.id,
        uuid: submitter.uuid,
        email: submitter.email,
        slug: submitter.slug,
        sent_at: submitter.sent_at,
        opened_at: submitter.opened_at,
        completed_at: submitter.completed_at,
        declined_at: nil,
        created_at: submitter.created_at,
        updated_at: submitter.updated_at,
        name: submitter.name,
        phone: submitter.phone,
        status: submitter.status,
        role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'],
        external_id: nil,
        application_key: nil, # Backward compatibility
        metadata: {},
        preferences: {}
      }
    end

    {
      id: submission.id,
      source: 'link',
      submitters_order: 'random',
      slug: submission.slug,
      audit_log_url: nil,
      combined_document_url: nil,
      expire_at: nil,
      completed_at: nil,
      created_at: submission.created_at,
      updated_at: submission.updated_at,
      archived_at: nil,
      status: 'pending',
      submitters:,
      template: {
        id: submission.template.id,
        name: submission.template.name,
        external_id: nil,
        folder_name: folder.name,
        created_at: submission.template.created_at,
        updated_at: submission.template.updated_at
      },
      created_by_user: {
        id: author.id,
        first_name: author.first_name,
        last_name: author.last_name,
        email: author.email
      }
    }
  end

  def show_submission_body(submission)
    submitters = submission.submitters.map do |submitter|
      {
        id: submitter.id,
        submission_id: submission.id,
        uuid: submitter.uuid,
        email: submitter.email,
        slug: submitter.slug,
        sent_at: submitter.sent_at,
        opened_at: submitter.opened_at,
        completed_at: submitter.completed_at,
        declined_at: nil,
        created_at: submitter.created_at,
        updated_at: submitter.updated_at,
        name: submitter.name,
        phone: submitter.phone,
        status: submitter.status,
        external_id: nil,
        application_key: nil, # Backward compatibility
        metadata: {},
        preferences: {},
        role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'],
        documents: [],
        values: []
      }
    end

    {
      id: submission.id,
      source: 'link',
      status: 'pending',
      submitters_order: 'random',
      slug: submission.slug,
      audit_log_url: nil,
      combined_document_url: nil,
      expire_at: nil,
      completed_at: nil,
      created_at: submission.created_at,
      updated_at: submission.updated_at,
      archived_at: nil,
      submitters:,
      template: {
        id: submission.template.id,
        name: submission.template.name,
        external_id: nil,
        folder_name: folder.name,
        created_at: submission.template.created_at,
        updated_at: submission.template.updated_at
      },
      created_by_user: {
        id: author.id,
        first_name: author.first_name,
        last_name: author.last_name,
        email: author.email
      },
      documents: [],
      submission_events: submission.submission_events.map do |event|
        {
          id: event.id,
          submitter_id: event.submitter_id,
          event_type: event.event_type,
          event_timestamp: event.event_timestamp,
          data: event.data.slice(:reason)
        }
      end
    }
  end

  def create_submission_body(submission)
    submission.submitters.map do |submitter|
      {
        id: submitter.id,
        submission_id: submission.id,
        uuid: submitter.uuid,
        email: submitter.email,
        slug: submitter.slug,
        sent_at: submitter.sent_at,
        opened_at: submitter.opened_at,
        completed_at: submitter.completed_at,
        declined_at: nil,
        created_at: submitter.created_at,
        updated_at: submitter.updated_at,
        name: submitter.name,
        phone: submitter.phone,
        status: submitter.status,
        external_id: nil,
        application_key: nil, # Backward compatibility
        metadata: {},
        preferences: { send_email: true, send_sms: false },
        role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'],
        embed_src: "#{Docuseal::DEFAULT_APP_URL}/s/#{submitter.slug}",
        values: []
      }
    end
  end
end