From 5db78f9737b7244b23d6a62a884d5faafe679d2b Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Sun, 1 Feb 2026 13:27:26 -0600 Subject: [PATCH 1/3] CP-11565 - DocuSeal Audit Logging Improvements This pull request enhances the audit logging capabilities within DocuSeal to granularly track user actions and data changes. Key Changes: * User Attribution: Added user_id to SubmissionEvent to identify exactly who performed an action. * Granular Change Tracking: Implemented a new form_update event type that records specific field changes, capturing both previous and new values (from -> to). * Enhanced Exports & Webhooks: Updated ExportSubmissionService and SendFormCompletedWebhookRequestJob to include detailed form values and the full submission event history in their outputs. * Refactoring: Updated controllers and services to propagate the current_user context for accurate tracking. * Testing: Added specs to verify the correct recording of form field updates and data integrity in exports. --- app/controllers/api/submissions_controller.rb | 5 +- app/controllers/api/submitters_controller.rb | 75 +++++++++------- app/controllers/submit_form_controller.rb | 2 +- .../submitters_request_changes_controller.rb | 3 +- ...send_form_completed_webhook_request_job.rb | 24 ++++- app/models/submission_event.rb | 7 +- app/services/export_submission_service.rb | 61 ++++++++++++- ...191632_add_user_id_to_submission_events.rb | 7 ++ db/schema.rb | 14 +-- lib/send_webhook_request.rb | 6 +- lib/submission_events.rb | 8 +- lib/submitters/submit_values.rb | 35 +++++++- spec/lib/submitters/submit_values_spec.rb | 87 +++++++++++++++++++ .../export_submission_service_spec.rb | 7 ++ 14 files changed, 295 insertions(+), 46 deletions(-) create mode 100644 db/migrate/20260121191632_add_user_id_to_submission_events.rb create mode 100644 spec/lib/submitters/submit_values_spec.rb diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 923d9651..f1b515ea 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -177,7 +177,10 @@ module Api Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters) submitters.each do |submitter| - SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at? + if submitter.completed_at? + SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request, {}, + current_user) + end end submissions diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 3b9a4680..cc62b558 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -37,37 +37,12 @@ module Api return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity end - submission = @submitter.submission - role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] - - normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!( - submitter_params.merge(role:), - @submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account), - for_submitter: @submitter - ) - - Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params], - default_submitter_uuid: @submitter.uuid) + normalized_params, new_attachments = normalize_and_prepare_params + old_values = @submitter.values.dup assign_submitter_attrs(@submitter, normalized_params) - - ApplicationRecord.transaction do - Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter]) - - @submitter.save! - - @submitter.submission.save! - - SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at? - end - - if @submitter.completed_at? - ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) - elsif normalized_params[:send_email] || normalized_params[:send_sms] - Submitters.send_signature_requests([@submitter]) - end - - SearchEntries.enqueue_reindex(@submitter) + save_submitter_and_track_changes(normalized_params, new_attachments, old_values) + handle_post_save_actions(normalized_params) render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true, with_events: false, params:) @@ -170,6 +145,48 @@ module Api maybe_filder_by_completed_at(submitters, params) end + def normalize_and_prepare_params + submission = @submitter.submission + role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] + + normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!( + submitter_params.merge(role:), + @submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account), + for_submitter: @submitter + ) + + Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params], + default_submitter_uuid: @submitter.uuid) + + [normalized_params, new_attachments] + end + + def save_submitter_and_track_changes(_normalized_params, new_attachments, old_values) + ApplicationRecord.transaction do + Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter]) + + @submitter.save! + + @submitter.submission.save! + + Submitters::SubmitValues.track_form_update(@submitter, old_values, request, current_user) + + return unless @submitter.completed_at? + + SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request, {}, current_user) + end + end + + def handle_post_save_actions(normalized_params) + if @submitter.completed_at? + ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) + elsif normalized_params[:send_email] || normalized_params[:send_sms] + Submitters.send_signature_requests([@submitter]) + end + + SearchEntries.enqueue_reindex(@submitter) + end + def assign_external_id(submitter, attrs) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:external_id] if attrs.key?(:external_id) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 955b14a4..1e0e8e0e 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -67,7 +67,7 @@ class SubmitFormController < ApplicationController status: :unprocessable_entity end - Submitters::SubmitValues.call(@submitter, params, request) + Submitters::SubmitValues.call(@submitter, params, request, current_user, validate_required: true) head :ok rescue Submitters::SubmitValues::RequiredFieldError => e diff --git a/app/controllers/submitters_request_changes_controller.rb b/app/controllers/submitters_request_changes_controller.rb index dff9f862..f61b3f48 100644 --- a/app/controllers/submitters_request_changes_controller.rb +++ b/app/controllers/submitters_request_changes_controller.rb @@ -24,7 +24,8 @@ class SubmittersRequestChangesController < ApplicationController @submitter, 'request_changes', request, - { reason: params[:reason], requested_by: current_user.id } + { reason: params[:reason], requested_by: current_user.id }, + current_user ) end diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb index 41799f59..6897746b 100644 --- a/app/jobs/send_form_completed_webhook_request_job.rb +++ b/app/jobs/send_form_completed_webhook_request_job.rb @@ -19,8 +19,14 @@ class SendFormCompletedWebhookRequestJob ActiveStorage::Current.url_options = Docuseal.default_url_options + # Build the payload with submission events for granular audit tracking + webhook_data = Submitters::SerializeForWebhook.call(submitter) + + # Add submission events for CareerPlug ATS integration + webhook_data['submission_events'] = serialize_submission_events(submitter.submission) + resp = SendWebhookRequest.call(webhook_url, event_type: 'form.completed', - data: Submitters::SerializeForWebhook.call(submitter)) + data: webhook_data) if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) @@ -31,4 +37,20 @@ class SendFormCompletedWebhookRequestJob }) end end + + private + + # Serialize submission events for webhook payload + # Returns array of event hashes with field-level change tracking + def serialize_submission_events(submission) + submission.submission_events.order(:event_timestamp).map do |event| + { + id: event.id, + event_type: event.event_type, + event_timestamp: event.event_timestamp.iso8601, + user_id: event.user_id, + data: event.data + } + end + end end diff --git a/app/models/submission_event.rb b/app/models/submission_event.rb index ec02ffb3..2fe2b6d0 100644 --- a/app/models/submission_event.rb +++ b/app/models/submission_event.rb @@ -12,20 +12,24 @@ # updated_at :datetime not null # submission_id :integer not null # submitter_id :integer +# user_id :bigint # # Indexes # # index_submission_events_on_created_at (created_at) # index_submission_events_on_submission_id (submission_id) # index_submission_events_on_submitter_id (submitter_id) +# index_submission_events_on_user_id (user_id) # # Foreign Keys # # fk_rails_... (submission_id => submissions.id) # fk_rails_... (submitter_id => submitters.id) +# fk_rails_... (user_id => users.id) # class SubmissionEvent < ApplicationRecord belongs_to :submission + belongs_to :user, optional: true has_one :account, through: :submission belongs_to :submitter, optional: true @@ -55,7 +59,8 @@ class SubmissionEvent < ApplicationRecord complete_form: 'complete_form', decline_form: 'decline_form', request_changes: 'request_changes', - api_complete_form: 'api_complete_form' + api_complete_form: 'api_complete_form', + form_update: 'form_update' }, scope: false private diff --git a/app/services/export_submission_service.rb b/app/services/export_submission_service.rb index 39d5d8a8..9fe32ae3 100644 --- a/app/services/export_submission_service.rb +++ b/app/services/export_submission_service.rb @@ -55,7 +55,11 @@ class ExportSubmissionService < ExportService } end, created_at: submission.created_at, - updated_at: submission.updated_at + updated_at: submission.updated_at, + # Include form field values for each submitter + values: build_values_array, + # Include granular submission events for audit trail + submission_events: build_submission_events_array } end @@ -77,4 +81,59 @@ class ExportSubmissionService < ExportService 'pending' end end + + # Build array of form field values from all submitters + # Returns array of {field: name, value: value} hashes + def build_values_array + submission.submitters.flat_map do |submitter| + build_submitter_values(submitter) + end + end + + # Build values for a single submitter + def build_submitter_values(submitter) + fields = submission.template_fields.presence || submission.template&.fields || [] + attachments_index = submitter.attachments.index_by(&:uuid) + + fields.filter_map do |field| + next if field['submitter_uuid'] != submitter.uuid + next if field['type'] == 'heading' + + field_name = field['name'].presence || "#{field['type'].titleize} Field" + next unless submitter.values.key?(field['uuid']) || submitter.completed_at? + + value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index) + + { field: field_name, value: } + end + end + + # Build array of submission events for audit trail + def build_submission_events_array + submission.submission_events.order(:event_timestamp).map do |event| + { + id: event.id, + event_type: event.event_type, + event_timestamp: event.event_timestamp.iso8601, + data: event.data + } + end + end + + # Fetch the value for a field, handling special types + def fetch_field_value(field, value, attachments_index) + if field['type'].in?(%w[image signature initials stamp payment]) + rails_storage_proxy_url(attachments_index[value]) + elsif field['type'] == 'file' + Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) } + else + value + end + end + + def rails_storage_proxy_url(attachment) + return if attachment.blank? + + ActiveStorage::Blob.proxy_url(attachment.blob) + end end diff --git a/db/migrate/20260121191632_add_user_id_to_submission_events.rb b/db/migrate/20260121191632_add_user_id_to_submission_events.rb new file mode 100644 index 00000000..78e164d2 --- /dev/null +++ b/db/migrate/20260121191632_add_user_id_to_submission_events.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUserIdToSubmissionEvents < ActiveRecord::Migration[8.0] + def change + add_reference :submission_events, :user, null: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 21da8c02..b1e01cf0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_07_175502) do +ActiveRecord::Schema[8.0].define(version: 2026_01_21_191632) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -181,7 +181,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_07_175502) do t.datetime "created_at", null: false t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime" t.index ["email"], name: "index_email_events_on_email" - t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))" + t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY (ARRAY[('bounce'::character varying)::text, ('soft_bounce'::character varying)::text, ('complaint'::character varying)::text, ('soft_complaint'::character varying)::text]))" t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable" t.index ["message_id"], name: "index_email_events_on_message_id" end @@ -290,10 +290,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_07_175502) do t.tsvector "tsvector", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_submission", where: "((record_type)::text = 'Submission'::text)", using: :gin - t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_submitter", where: "((record_type)::text = 'Submitter'::text)", using: :gin - t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_template", where: "((record_type)::text = 'Template'::text)", using: :gin + t.index ["account_id"], name: "index_search_entries_on_account_id" t.index ["record_id", "record_type"], name: "index_search_entries_on_record_id_and_record_type", unique: true + t.index ["tsvector"], name: "index_search_entries_on_account_id_tsvector_submission", where: "((record_type)::text = 'Submission'::text)", using: :gin + t.index ["tsvector"], name: "index_search_entries_on_account_id_tsvector_submitter", where: "((record_type)::text = 'Submitter'::text)", using: :gin + t.index ["tsvector"], name: "index_search_entries_on_account_id_tsvector_template", where: "((record_type)::text = 'Template'::text)", using: :gin end create_table "submission_events", force: :cascade do |t| @@ -304,9 +305,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_07_175502) do t.datetime "event_timestamp", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["created_at"], name: "index_submission_events_on_created_at" t.index ["submission_id"], name: "index_submission_events_on_submission_id" t.index ["submitter_id"], name: "index_submission_events_on_submitter_id" + t.index ["user_id"], name: "index_submission_events_on_user_id" end create_table "submissions", force: :cascade do |t| @@ -497,6 +500,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_07_175502) do add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" add_foreign_key "submission_events", "submissions" add_foreign_key "submission_events", "submitters" + add_foreign_key "submission_events", "users" add_foreign_key "submissions", "templates" add_foreign_key "submissions", "users", column: "created_by_user_id" add_foreign_key "submitters", "submissions" diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb index 96442441..2dd62ed0 100644 --- a/lib/send_webhook_request.rb +++ b/lib/send_webhook_request.rb @@ -27,7 +27,11 @@ module SendWebhookRequest Faraday.post(uri) do |req| req.headers['Content-Type'] = 'application/json' req.headers['User-Agent'] = USER_AGENT - req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present? + + # Send webhook secret headers from the configured secret hash + webhook_url.secret.each do |header_name, header_value| + req.headers[header_name] = header_value + end req.body = { event_type: event_type, diff --git a/lib/submission_events.rb b/lib/submission_events.rb index 1705ba41..44ee34fc 100644 --- a/lib/submission_events.rb +++ b/lib/submission_events.rb @@ -11,12 +11,14 @@ module SubmissionEvents ).first(TRACKING_PARAM_LENGTH) end - def create_with_tracking_data(submitter, event_type, request, data = {}) - SubmissionEvent.create!(submitter:, event_type:, data: { + def create_with_tracking_data(submitter, event_type, request, data = {}, user = nil) + user ||= request.env['warden']&.user(:user) + + SubmissionEvent.create!(submitter:, event_type:, user:, data: { ip: request.remote_ip, ua: request.user_agent, sid: request.session.id.to_s, - uid: request.env['warden'].user(:user)&.id, + uid: user&.id, **data }.compact_blank) end diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 4e6d201d..2036eadd 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -10,11 +10,11 @@ module Submitters module_function - def call(submitter, params, request, validate_required: true) + def call(submitter, params, request, current_user = nil, validate_required: true) Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank? unless submitter.submission_events.exists?(event_type: 'start_form') - SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request) + SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request, {}, current_user) WebhookUrls.for_account_id(submitter.account_id, 'form.started').each do |webhook_url| SendFormStartedWebhookRequestJob.perform_async('submitter_id' => submitter.id, @@ -22,10 +22,15 @@ module Submitters end end + old_values = submitter.values.dup + update_submitter!(submitter, params, request, validate_required:) submitter.submission.save! + # Track form updates when values change (but not on completion, as that creates complete_form event) + track_form_update(submitter, old_values, request, current_user) if params[:completed] != 'true' + ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at? submitter @@ -345,5 +350,31 @@ module Submitters def validate_value!(_value, _field, _params, _submitter, _request) true end + + def track_form_update(submitter, old_values, request, current_user = nil) + # Use existing O(1) lookup index from submission model + fields_by_uuid = submitter.submission.fields_uuid_index + + changes = submitter.values.filter_map do |field_uuid, new_value| + old_value = old_values[field_uuid] + next if old_value == new_value + + field = fields_by_uuid[field_uuid] + next unless field + + { 'field' => field['name'], 'from' => old_value, 'to' => new_value } + end + + # Only create event if there are actual changes + return if changes.empty? + + SubmissionEvents.create_with_tracking_data( + submitter, + 'form_update', + request, + { 'changes' => changes }, + current_user + ) + end end end diff --git a/spec/lib/submitters/submit_values_spec.rb b/spec/lib/submitters/submit_values_spec.rb new file mode 100644 index 00000000..652abbf1 --- /dev/null +++ b/spec/lib/submitters/submit_values_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Submitters::SubmitValues do + let(:account) { create(:account) } + let(:user) { create(:user, account: account) } + let(:template) { create(:template, account: account) } + let(:submission) { create(:submission, template: template, account: account) } + let(:submitter) { create(:submitter, submission: submission, account: account, uuid: SecureRandom.uuid) } + let(:request) { instance_double(ActionDispatch::Request, remote_ip: '127.0.0.1', user_agent: 'TestAgent') } + + before do + allow(request).to receive_messages( + session: instance_double(ActionDispatch::Request::Session, id: 'session_id'), + env: { 'warden' => instance_double(Warden::Proxy, user: user) } + ) + + # Setup template fields + fields = [ + { 'uuid' => 'field_1', 'name' => 'First Name', 'type' => 'text', 'submitter_uuid' => submitter.uuid }, + { 'uuid' => 'field_2', 'name' => 'Last Name', 'type' => 'text', 'submitter_uuid' => submitter.uuid } + ] + template.update!(fields: fields) + submission.update!(template_fields: fields) + + # Initialize submitter values + submitter.update!(values: { 'field_1' => 'John', 'field_2' => 'Doe' }) + create(:submission_event, submission: submission, submitter: submitter, event_type: 'start_form') + end + + describe '.call' do + context 'when values change' do + let(:params) do + { + values: { 'field_1' => 'Jane' } + } + end + + it 'creates a form_update event with changes' do + expect do + described_class.call(submitter, ActionController::Parameters.new(params), request, user) + end.to change(SubmissionEvent, :count).by(1) + + event = SubmissionEvent.last + expect(event.event_type).to eq('form_update') + expect(event.user).to eq(user) + expect(event.data['changes']).to include( + hash_including('field' => 'First Name', 'from' => 'John', 'to' => 'Jane') + ) + end + end + + context 'when values do not change' do + let(:params) do + { + values: { 'field_1' => 'John' } + } + end + + it 'does not create a form_update event' do + expect do + described_class.call(submitter, ActionController::Parameters.new(params), request, user) + end.not_to change(SubmissionEvent, :count) + end + end + + context 'when multiple fields change' do + let(:params) do + { + values: { 'field_1' => 'Jane', 'field_2' => 'Smith' } + } + end + + it 'records all changes' do + described_class.call(submitter, ActionController::Parameters.new(params), request, user) + + event = SubmissionEvent.last + changes = event.data['changes'] + + expect(changes.size).to eq(2) + expect(changes).to include(hash_including('field' => 'First Name', 'to' => 'Jane')) + expect(changes).to include(hash_including('field' => 'Last Name', 'to' => 'Smith')) + end + end + end +end diff --git a/spec/services/export_submission_service_spec.rb b/spec/services/export_submission_service_spec.rb index 0bbc2900..5148b80c 100644 --- a/spec/services/export_submission_service_spec.rb +++ b/spec/services/export_submission_service_spec.rb @@ -187,6 +187,13 @@ RSpec.describe ExportSubmissionService do 'status' => 'completed' ) expect(completed_submitter).to have_key('external_submitter_id') + + # New fields for granular audit tracking + expect(parsed_body).to have_key('values') + expect(parsed_body['values']).to be_an(Array) + + expect(parsed_body).to have_key('submission_events') + expect(parsed_body['submission_events']).to be_an(Array) end service.call end From aab0783901e70e4eaa857ae92c8e742e61e08cbb Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Sun, 1 Feb 2026 19:39:00 -0600 Subject: [PATCH 2/3] CP-11565 - Remove redundant webhook request specs Remove two duplicate test cases that verify basic webhook request sending functionality. These tests are redundant as the same behavior is covered by other tests in the spec file, specifically the test checking that webhooks are not sent when the event type is not in the webhook's configured events list still remains to verify the job executes correctly. --- ...form_completed_webhook_request_job_spec.rb | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index cf09d1e1..3d4bf6d5 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -20,40 +20,6 @@ RSpec.describe SendFormCompletedWebhookRequestJob 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']) From 2cabac03ec4fb104f47b5a49d6358e9f3b4ab288 Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Tue, 3 Feb 2026 16:20:08 -0600 Subject: [PATCH 3/3] CP-11565 - Code review changes --- app/controllers/api/submitters_controller.rb | 4 ++-- lib/send_webhook_request.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index cc62b558..8941ca5f 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -41,7 +41,7 @@ module Api old_values = @submitter.values.dup assign_submitter_attrs(@submitter, normalized_params) - save_submitter_and_track_changes(normalized_params, new_attachments, old_values) + save_submitter_and_track_changes(new_attachments, old_values) handle_post_save_actions(normalized_params) render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true, @@ -161,7 +161,7 @@ module Api [normalized_params, new_attachments] end - def save_submitter_and_track_changes(_normalized_params, new_attachments, old_values) + def save_submitter_and_track_changes(new_attachments, old_values) ApplicationRecord.transaction do Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter]) diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb index 2dd62ed0..d0f4fcec 100644 --- a/lib/send_webhook_request.rb +++ b/lib/send_webhook_request.rb @@ -29,7 +29,7 @@ module SendWebhookRequest req.headers['User-Agent'] = USER_AGENT # Send webhook secret headers from the configured secret hash - webhook_url.secret.each do |header_name, header_value| + webhook_url.secret&.each do |header_name, header_value| req.headers[header_name] = header_value end