diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb
index bda564c0c..6337a1236 100644
--- a/app/controllers/api/submissions_controller.rb
+++ b/app/controllers/api/submissions_controller.rb
@@ -67,15 +67,18 @@ def create
submissions = create_submissions(@template, params)
- submissions.each do |submission|
- SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
+ WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
+ submissions.each do |submission|
+ SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
end
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
submission.submitters.each do |submitter|
- ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
+ ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
end
end
@@ -93,7 +96,10 @@ def destroy
else
@submission.update!(archived_at: Time.current)
- SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
+ WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
+ SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
end
render json: @submission.as_json(only: %i[id archived_at])
diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb
index b582095a4..98d7f5b52 100644
--- a/app/controllers/api/submitter_form_views_controller.rb
+++ b/app/controllers/api/submitter_form_views_controller.rb
@@ -13,7 +13,10 @@ def create
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
- SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
+ WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
+ SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
render json: {}
end
diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb
index 210b783d5..f85e2f60a 100644
--- a/app/controllers/api/submitters_controller.rb
+++ b/app/controllers/api/submitters_controller.rb
@@ -68,7 +68,7 @@ def update
end
if @submitter.completed_at?
- ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
+ ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb
index 52c0c30bc..f869fa28a 100644
--- a/app/controllers/api/templates_clone_controller.rb
+++ b/app/controllers/api/templates_clone_controller.rb
@@ -25,7 +25,10 @@ def create
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
- SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
+ WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
+ SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end
diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb
index add4bcf85..80af5acf9 100644
--- a/app/controllers/api/templates_controller.rb
+++ b/app/controllers/api/templates_controller.rb
@@ -65,7 +65,10 @@ def update
@template.update!(template_params)
- SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
+ WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
+ SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
render json: @template.as_json(only: %i[id updated_at])
end
diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb
index c7fc9ae8f..209c81b49 100644
--- a/app/controllers/start_form_controller.rb
+++ b/app/controllers/start_form_controller.rb
@@ -38,7 +38,10 @@ def update
if @submitter.save
if is_new_record
- SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
+ WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
+ SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
+ 'webhook_url_id' => webhook_url.id)
+ end
end
redirect_to submit_form_path(@submitter.slug)
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 6f92a874d..a7a7f216a 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -50,9 +50,7 @@ def create
params: params.merge('send_completed_email' => true))
end
- submissions.each do |submission|
- SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
- end
+ enqueue_submission_created_webhooks(@template, submissions)
Submissions.send_signature_requests(submissions)
@@ -68,7 +66,10 @@ def destroy
else
@submission.update!(archived_at: Time.current)
- SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
+ WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
+ SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
I18n.t('submission_has_been_archived')
end
@@ -85,6 +86,15 @@ def save_template_message(template, params)
template.save!
end
+ def enqueue_submission_created_webhooks(template, submissions)
+ WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
+ submissions.each do |submission|
+ SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
+ end
+ end
+
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
end
diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb
index a04f5ca28..94099b0df 100644
--- a/app/controllers/submit_form_decline_controller.rb
+++ b/app/controllers/submit_form_decline_controller.rb
@@ -25,7 +25,10 @@ def create
SubmitterMailer.declined_email(submitter, user).deliver_later!
end
- SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
+ WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
+ SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
redirect_to submit_form_path(submitter.slug)
end
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index d5c06ace2..5a578685b 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -66,7 +66,7 @@ def create
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
- SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
+ enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template)
else
@@ -77,7 +77,7 @@ def create
def update
@template.update!(template_params)
- SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
+ enqueue_template_updated_webhooks(@template)
head :ok
end
@@ -128,6 +128,20 @@ def maybe_redirect_to_template(template)
end
end
+ def enqueue_template_created_webhooks(template)
+ WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
+ SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
+ end
+
+ def enqueue_template_updated_webhooks(template)
+ WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
+ SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
+ end
+
def load_base_template
return if params[:base_template_id].blank?
diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb
index daefcc678..75b4e620a 100644
--- a/app/controllers/templates_uploads_controller.rb
+++ b/app/controllers/templates_uploads_controller.rb
@@ -23,7 +23,7 @@ def create
@template.update!(schema:)
- SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
+ enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
@@ -65,4 +65,11 @@ def create_file_params_from_url
{ files: [file] }
end
+
+ def enqueue_template_created_webhooks(template)
+ WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
+ SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
+ end
end
diff --git a/app/controllers/testing_api_settings_controller.rb b/app/controllers/testing_api_settings_controller.rb
index 5eade6479..6c34fed7f 100644
--- a/app/controllers/testing_api_settings_controller.rb
+++ b/app/controllers/testing_api_settings_controller.rb
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
- @webhook_config =
- current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
+ @webhook_url = current_account.webhook_urls.first_or_initialize
- authorize!(:manage, @webhook_config)
+ authorize!(:manage, @webhook_url)
end
end
diff --git a/app/controllers/webhook_preferences_controller.rb b/app/controllers/webhook_preferences_controller.rb
index d584fdf9a..1070f7fe4 100644
--- a/app/controllers/webhook_preferences_controller.rb
+++ b/app/controllers/webhook_preferences_controller.rb
@@ -1,37 +1,22 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
- EVENTS = %w[
- form.viewed
- form.started
- form.completed
- form.declined
- template.created
- template.updated
- submission.created
- submission.archived
- ].freeze
+ load_and_authorize_resource :webhook_url, parent: false
- before_action :load_account_config
- authorize_resource :account_config, parent: false
+ def update
+ webhook_preferences_params[:events].each do |event, val|
+ @webhook_url.events.delete(event) if val == '0'
+ @webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
+ end
- def create
- @account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
-
- @account_config.save!
+ @webhook_url.save!
head :ok
end
private
- def load_account_config
- @account_config =
- current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
- @account_config.value ||= {}
- end
-
- def account_config_params
- params.permit(:event, :value)
+ def webhook_preferences_params
+ params.require(:webhook_url).permit(events: {})
end
end
diff --git a/app/controllers/webhook_secret_controller.rb b/app/controllers/webhook_secret_controller.rb
index cd5bbefa2..27f35b260 100644
--- a/app/controllers/webhook_secret_controller.rb
+++ b/app/controllers/webhook_secret_controller.rb
@@ -1,29 +1,21 @@
# frozen_string_literal: true
class WebhookSecretController < ApplicationController
- before_action :load_encrypted_config
- authorize_resource :encrypted_config, parent: false
+ load_and_authorize_resource :webhook_url, parent: false
- def index; end
+ def show; end
- def create
- @encrypted_config.assign_attributes(value: {
- encrypted_config_params[:key] => encrypted_config_params[:value]
+ def update
+ @webhook_url.update!(secret: {
+ webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank)
- @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
-
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end
private
- def load_encrypted_config
- @encrypted_config =
- current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
- end
-
- def encrypted_config_params
- params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
+ def webhook_secret_params
+ params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
end
end
diff --git a/app/controllers/webhook_settings_controller.rb b/app/controllers/webhook_settings_controller.rb
index 475e830f6..2c6362af3 100644
--- a/app/controllers/webhook_settings_controller.rb
+++ b/app/controllers/webhook_settings_controller.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
- before_action :load_encrypted_config
- authorize_resource :encrypted_config, parent: false
+ before_action :load_webhook_url
+ authorize_resource :webhook_url, parent: false
def show; end
def create
- @encrypted_config.assign_attributes(encrypted_config_params)
+ @webhook_url.assign_attributes(webhook_params)
- @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
+ @webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end
@@ -17,20 +17,19 @@ def create
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
- SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
- 'encrypted_config_id' => @encrypted_config.id })
+ SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
+ 'webhook_url_id' => @webhook_url.id)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end
private
- def load_encrypted_config
- @encrypted_config =
- current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
+ def load_webhook_url
+ @webhook_url = current_account.webhook_urls.first_or_initialize
end
- def encrypted_config_params
- params.require(:encrypted_config).permit(:value)
+ def webhook_params
+ params.require(:webhook_url).permit(:url)
end
end
diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb
index 19c278d64..c23c7f827 100644
--- a/app/jobs/process_submitter_completion_job.rb
+++ b/app/jobs/process_submitter_completion_job.rb
@@ -63,32 +63,15 @@ def create_completed_documents!(submitter)
end
def enqueue_completed_webhooks(submitter, is_all_completed: false)
- webhook_config = Accounts.load_webhook_config(submitter.account)
-
- if webhook_config
- SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
- 'encrypted_config_id' => webhook_config.id })
- end
-
- webhook_urls = submitter.account.webhook_urls
-
- webhook_urls = webhook_urls.where(
- Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
- ).or(
- webhook_urls.where(
- Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
- )
- )
-
- webhook_urls.each do |webhook|
+ WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
if webhook.events.include?('form.completed')
- SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
- 'webhook_url_id' => webhook.id })
+ SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook.id)
end
if webhook.events.include?('submission.completed') && is_all_completed
- SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id,
- 'webhook_url_id' => webhook.id })
+ SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
+ 'webhook_url_id' => webhook.id)
end
end
end
diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb
index 62c6e1d57..30810c328 100644
--- a/app/jobs/send_form_completed_webhook_request_job.rb
+++ b/app/jobs/send_form_completed_webhook_request_job.rb
@@ -5,31 +5,30 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- url, secret = load_url_and_secret(submitter, params)
-
- return if url.blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed')
Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
- **secret.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -45,27 +44,4 @@ def perform(params = {})
})
end
end
-
- def load_url_and_secret(submitter, params)
- if params['encrypted_config_id']
- config = EncryptedConfig.find(params['encrypted_config_id'])
-
- url = config.value
-
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submitter.submission.account)
-
- return if preferences['form.completed'] == false
-
- secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h
-
- [url, secret]
- elsif params['webhook_url_id']
- webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id'])
-
- webhook_url.url if webhook_url.events.include?('form.completed')
- end
- end
end
diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb
index b4f158b6d..86fdae2aa 100644
--- a/app/jobs/send_form_declined_webhook_request_job.rb
+++ b/app/jobs/send_form_declined_webhook_request_job.rb
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(submitter.submission.account)
- url = config&.value.presence
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submitter.submission.account)
-
- return if preferences['form.declined'] == false
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'form.declined',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ def perform(params = {})
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_form_started_webhook_request_job.rb b/app/jobs/send_form_started_webhook_request_job.rb
index 747e93388..3a6e0eaeb 100644
--- a/app/jobs/send_form_started_webhook_request_job.rb
+++ b/app/jobs/send_form_started_webhook_request_job.rb
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(submitter.submission.account)
- url = config&.value.presence
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submitter.submission.account)
-
- return if preferences['form.started'] == false
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ def perform(params = {})
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_form_viewed_webhook_request_job.rb b/app/jobs/send_form_viewed_webhook_request_job.rb
index 391064e31..c70cf776c 100644
--- a/app/jobs/send_form_viewed_webhook_request_job.rb
+++ b/app/jobs/send_form_viewed_webhook_request_job.rb
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(submitter.submission.account)
- url = config&.value.presence
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submitter.submission.account)
-
- return if preferences['form.viewed'] == false
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ def perform(params = {})
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_submission_archived_webhook_request_job.rb b/app/jobs/send_submission_archived_webhook_request_job.rb
index ff62419eb..334d047b0 100644
--- a/app/jobs/send_submission_archived_webhook_request_job.rb
+++ b/app/jobs/send_submission_archived_webhook_request_job.rb
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(submission.account)
- url = config&.value.presence
-
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submission.account)
-
- return if preferences['submission.archived'].blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ def perform(params = {})
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_submission_completed_webhook_request_job.rb b/app/jobs/send_submission_completed_webhook_request_job.rb
index 5a58c514a..085e721c1 100644
--- a/app/jobs/send_submission_completed_webhook_request_job.rb
+++ b/app/jobs/send_submission_completed_webhook_request_job.rb
@@ -5,30 +5,26 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- webhook_url = submission.account.webhook_urls.find(params['webhook_url_id'])
-
- url = webhook_url.url if webhook_url.events.include?('submission.completed')
-
- return if url.blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'submission.completed',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
diff --git a/app/jobs/send_submission_created_webhook_request_job.rb b/app/jobs/send_submission_created_webhook_request_job.rb
index aeb3f18ff..a4dba4a67 100644
--- a/app/jobs/send_submission_created_webhook_request_job.rb
+++ b/app/jobs/send_submission_created_webhook_request_job.rb
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(submission.account)
- url = config&.value.presence
-
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(submission.account)
-
- return if preferences['submission.created'].blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'submission.created',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ def perform(params = {})
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_template_created_webhook_request_job.rb b/app/jobs/send_template_created_webhook_request_job.rb
index e2d7ac714..6b2493dc6 100644
--- a/app/jobs/send_template_created_webhook_request_job.rb
+++ b/app/jobs/send_template_created_webhook_request_job.rb
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(template.account)
- url = config&.value.presence
-
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(template.account)
-
- return if preferences['template.created'].blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'template.created',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ def perform(params = {})
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/jobs/send_template_updated_webhook_request_job.rb b/app/jobs/send_template_updated_webhook_request_job.rb
index 8d969eb5b..68479f1b5 100644
--- a/app/jobs/send_template_updated_webhook_request_job.rb
+++ b/app/jobs/send_template_updated_webhook_request_job.rb
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks
- USER_AGENT = 'DocuSeal.co Webhook'
+ USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
+ webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
- config = Accounts.load_webhook_config(template.account)
- url = config&.value.presence
-
- return if url.blank?
-
- preferences = Accounts.load_webhook_preferences(template.account)
-
- return if preferences['template.updated'].blank?
+ return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
resp = begin
- Faraday.post(url,
+ Faraday.post(webhook_url.url,
{
event_type: 'template.updated',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
- **EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
- key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
+ **webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ def perform(params = {})
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
+ 'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
diff --git a/app/models/account_config.rb b/app/models/account_config.rb
index 1e7e2d617..112ca4666 100644
--- a/app/models/account_config.rb
+++ b/app/models/account_config.rb
@@ -35,7 +35,6 @@ class AccountConfig < ApplicationRecord
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
- WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
diff --git a/app/models/encrypted_config.rb b/app/models/encrypted_config.rb
index 5d2422af9..e61923b4f 100644
--- a/app/models/encrypted_config.rb
+++ b/app/models/encrypted_config.rb
@@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
- APP_URL_KEY = 'app_url',
- WEBHOOK_URL_KEY = 'webhook_url',
- WEBHOOK_SECRET_KEY = 'webhook_secret'
+ APP_URL_KEY = 'app_url'
].freeze
belongs_to :account
diff --git a/app/models/webhook_url.rb b/app/models/webhook_url.rb
index 74d70bb47..011bf5a1c 100644
--- a/app/models/webhook_url.rb
+++ b/app/models/webhook_url.rb
@@ -6,6 +6,7 @@
#
# id :bigint not null, primary key
# events :text not null
+# secret :text not null
# sha1 :string not null
# url :text not null
# created_at :datetime not null
@@ -22,15 +23,28 @@
# fk_rails_... (account_id => accounts.id)
#
class WebhookUrl < ApplicationRecord
+ EVENTS = %w[
+ form.viewed
+ form.started
+ form.completed
+ form.declined
+ template.created
+ template.updated
+ submission.created
+ submission.archived
+ ].freeze
+
belongs_to :account
- attribute :events, :string, default: -> { [] }
+ attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
+ attribute :secret, :string, default: -> { {} }
serialize :events, coder: JSON
+ serialize :secret, coder: JSON
before_validation :set_sha1
- encrypts :url
+ encrypts :url, :secret
def set_sha1
self.sha1 = Digest::SHA1.hexdigest(url)
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index baca3e563..0cbd57e82 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -1,3 +1,4 @@
+<%= content_for(:canonical_url, new_user_session_url) %>
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<%= t('sign_in') %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 4de5c8c76..590e0b23a 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -11,6 +11,9 @@
<% else %>
<%= javascript_pack_tag 'application', defer: true %>
<% end %>
+ <% if canonical_url = content_for(:canonical_url) %>
+
+ <% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb
index f70791c3d..fddae67f3 100644
--- a/app/views/shared/_settings_nav.html.erb
+++ b/app/views/shared/_settings_nav.html.erb
@@ -56,7 +56,7 @@
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
<% end %>
- <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %>
+ <% if can?(:read, WebhookUrl) %>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
@@ -72,13 +72,13 @@
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
- <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
+ <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
<% if Docuseal.multitenant? %>
- <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
+ <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= t('embedding') %>
<% end %>
diff --git a/app/views/testing_api_settings/index.html.erb b/app/views/testing_api_settings/index.html.erb
index c85f2613f..b02cad316 100644
--- a/app/views/testing_api_settings/index.html.erb
+++ b/app/views/testing_api_settings/index.html.erb
@@ -8,10 +8,10 @@
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
- <%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
- <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
+ <%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
+ <%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
- <%= f.url_field :value, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
+ <%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
<% end %>
diff --git a/app/views/webhook_secret/index.html.erb b/app/views/webhook_secret/show.erb
similarity index 50%
rename from app/views/webhook_secret/index.html.erb
rename to app/views/webhook_secret/show.erb
index 5c7ad9edb..88b996c5e 100644
--- a/app/views/webhook_secret/index.html.erb
+++ b/app/views/webhook_secret/show.erb
@@ -1,13 +1,13 @@
-<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %>
- <%= form_for @encrypted_config, url: webhook_secret_index_path, method: :post, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
+<%= render 'shared/turbo_modal', title: t('webhook_secret') do %>
+ <%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
- <%= f.fields_for :value, Struct.new(:key, :value).new(*@encrypted_config.value.to_a.first) do |ff| %>
+ <%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
- <%= ff.label :key, class: 'label' %>
+ <%= ff.label :key, t('key'), class: 'label' %>
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
- <%= ff.label :value, class: 'label' %>
+ <%= ff.label :value, t('value'), class: 'label' %>
<%= ff.text_field :value, class: 'base-input' %>
<% end %>
diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb
index 746d3958e..6c305d88d 100644
--- a/app/views/webhook_settings/show.html.erb
+++ b/app/views/webhook_settings/show.html.erb
@@ -7,34 +7,35 @@
- <%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
- <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
+ <%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
+ <%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<% end %>
- <% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
- <% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
-
- <% events.each do |event| %>
- <%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
- <%= f.hidden_field :event, value: event %>
- <% uuid = SecureRandom.uuid %>
-
-
-
+ <%= form_for @webhook_url, url: @webhook_url.url.present? ? webhook_preference_path(@webhook_url) : '', method: :put, html: { autocomplete: 'off' } do |f| %>
+ <% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
+
+ <% events.each do |event| %>
+ <%= f.fields_for :events do |ff| %>
+
+
+
+ <% end %>
<% end %>
- <% end %>
-
+
+ <% end %>
<% end %>
@@ -47,7 +48,7 @@
<%= t('submission_example_payload') %>
- <% if @encrypted_config.value.present? %>
+ <% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
<% end %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index 0ac26e8ab..e92205909 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -474,6 +474,7 @@ en: &en
logo: Logo
back: Back
add_secret: Add Secret
+ edit_secret: Edit Secret
submission_example_payload: Submission example payload
there_are_no_signatures: There are no signatures
signed_with_trusted_certificate: Signed with trusted certificate
@@ -623,7 +624,9 @@ en: &en
archived_users: Archived Users
embedding_users: Embedding Users
view_embedding_users: View Embedding Users
- view_users: View Users
+ key: Key
+ value: Value
+ webhook_secret: Webhook Secret
submission_event_names:
send_email_to_html: 'Email sent to %{submitter_name}'
send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}'
@@ -1116,6 +1119,7 @@ es: &es
logo: Logotipo
back: Atrás
add_secret: Agregar secreto
+ edit_secret: Editar secreto
submission_example_payload: Ejemplo de payload de envío
there_are_no_signatures: No hay firmas
signed_with_trusted_certificate: Firmado con certificado de confianza
@@ -1265,6 +1269,9 @@ es: &es
archived_users: Usuarios Archivados
embedding_users: Usuarios Integrados
view_embedding_users: Ver Usuarios Integrado
+ key: Clave
+ value: Valor
+ webhook_secret: Secreto del Webhook
submission_event_names:
send_email_to_html: 'Correo electrónico enviado a %{submitter_name}'
send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}'
@@ -1757,6 +1764,7 @@ it: &it
logo: Logo
back: Indietro
add_secret: Aggiungi segreto
+ edit_secret: Modifica segreto
submission_example_payload: Esempio di payload di invio
there_are_no_signatures: Non ci sono firme
signed_with_trusted_certificate: Firmato con certificato affidabile
@@ -1906,6 +1914,9 @@ it: &it
archived_users: Utenti Archiviati
embedding_users: Utenti Incorporati
view_embedding_users: Visualizza Utenti Incorporati
+ key: Chiave
+ value: Valore
+ webhook_secret: Segreto del Webhook
submission_event_names:
send_email_to_html: 'E-mail inviato a %{submitter_name}'
send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}'
@@ -2399,6 +2410,7 @@ fr: &fr
logo: Logo
back: Retour
add_secret: Ajouter un secret
+ edit_secret: Modifier le Secret
submission_example_payload: Exemple de payload de soumission
there_are_no_signatures: "Il n'y a pas de signatures"
signed_with_trusted_certificate: Signé avec un certificat de confiance
@@ -2548,6 +2560,9 @@ fr: &fr
archived_users: Utilisateurs Archivés
embedding_users: Utilisateurs Intégrés
view_embedding_users: Voir les Utilisateurs Intégrés
+ key: Clé
+ value: Valeur
+ webhook_secret: Secret du Webhook
submission_event_names:
send_email_to_html: 'E-mail envoyé à %{submitter_name}'
send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}'
@@ -3040,6 +3055,7 @@ pt: &pt
logo: Logotipo
back: Voltar
add_secret: Adicionar segredo
+ edit_secret: Editar Segredo
submission_example_payload: Exemplo de payload de submissão
there_are_no_signatures: Não há assinaturas
signed_with_trusted_certificate: Assinado com certificado confiável
@@ -3189,6 +3205,9 @@ pt: &pt
archived_users: Usuários Arquivados
embedding_users: Usuários Incorporados
view_embedding_users: Ver Usuários Incorporados
+ key: Chave
+ value: Valor
+ webhook_secret: Segredo do Webhook
submission_event_names:
send_email_to_html: 'E-mail enviado para %{submitter_name}'
send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}'
@@ -3681,6 +3700,7 @@ de: &de
logo: Logo
back: Zurück
add_secret: Geheimnis hinzufügen
+ edit_secret: Geheimnis bearbeiten
submission_example_payload: Beispiel-Payload für Einreichung
there_are_no_signatures: Es gibt keine Unterschriften
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
@@ -3830,6 +3850,9 @@ de: &de
archived_users: Archivierte Benutzer
embedding_users: Einbettende Benutzer
view_embedding_users: Einbettende Benutzer anzeigen
+ key: Schlüssel
+ value: Wert
+ webhook_secret: Webhook-Geheimnis
submission_event_names:
send_email_to_html: 'E-Mail gesendet an %{submitter_name}'
send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}'
diff --git a/config/routes.rb b/config/routes.rb
index 420859fcb..a2d5af4b0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -80,8 +80,8 @@
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index]
- resources :webhook_preferences, only: %i[create]
- resources :webhook_secret, only: %i[index create]
+ resources :webhook_secret, only: %i[show update]
+ resources :webhook_preferences, only: %i[update]
resource :templates_upload, only: %i[create]
authenticated do
resource :templates_upload, only: %i[show], path: 'new'
diff --git a/db/migrate/20241028162000_add_secret_to_webhook_urls.rb b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb
new file mode 100644
index 000000000..5da2b6bda
--- /dev/null
+++ b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2]
+ class MigrationWebhookUrl < ApplicationRecord
+ self.table_name = 'webhook_urls'
+
+ serialize :secret, coder: JSON
+
+ encrypts :url, :secret
+ end
+
+ def change
+ add_column :webhook_urls, :secret, :text
+
+ MigrationWebhookUrl.all.each do |url|
+ url.update_columns(secret: {})
+ end
+
+ change_column_null :webhook_urls, :secret, false
+ end
+end
diff --git a/db/migrate/20241029192232_populate_webhook_urls.rb b/db/migrate/20241029192232_populate_webhook_urls.rb
new file mode 100644
index 000000000..cb8b55460
--- /dev/null
+++ b/db/migrate/20241029192232_populate_webhook_urls.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction
+
+ class MigrationWebhookUrl < ActiveRecord::Base
+ self.table_name = 'webhook_urls'
+
+ serialize :events, coder: JSON
+ serialize :secret, coder: JSON
+
+ encrypts :url, :secret
+ end
+
+ class MigrationEncryptedConfig < ActiveRecord::Base
+ self.table_name = 'encrypted_configs'
+
+ encrypts :value
+ serialize :value, coder: JSON
+ end
+
+ class MigrationAccountConfig < ActiveRecord::Base
+ self.table_name = 'account_configs'
+
+ serialize :value, coder: JSON
+ end
+
+ def up
+ MigrationEncryptedConfig.where(key: 'webhook_url').find_each do |config|
+ webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id,
+ sha1: Digest::SHA1.hexdigest(config.value))
+
+ webhook_url.secret =
+ MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value.to_h
+
+ preferences =
+ MigrationAccountConfig.find_by(account_id: config.account_id, key: 'webhook_preferences')&.value.to_h
+
+ events = %w[form.viewed form.started form.completed form.declined].reject { |event| preferences[event] == false }
+
+ events += preferences.compact_blank.keys
+
+ webhook_url.events = events.uniq
+ webhook_url.url = config.value
+
+ webhook_url.save!
+ end
+ end
+
+ def down
+ nil
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3fd0ab117..92e6a9829 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[7.2].define(version: 2024_10_26_161207) do
+ActiveRecord::Schema[7.2].define(version: 2024_10_29_192232) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -367,6 +367,7 @@
t.string "sha1", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.text "secret", null: false
t.index ["account_id"], name: "index_webhook_urls_on_account_id"
t.index ["sha1"], name: "index_webhook_urls_on_sha1"
end
diff --git a/lib/ability.rb b/lib/ability.rb
index b48e866a5..960a8beba 100644
--- a/lib/ability.rb
+++ b/lib/ability.rb
@@ -22,5 +22,6 @@ def initialize(user)
can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id
can :manage, AccessToken, user_id: user.id
+ can :manage, WebhookUrl, account_id: user.account_id
end
end
diff --git a/lib/accounts.rb b/lib/accounts.rb
index 644032b1e..67427cf6d 100644
--- a/lib/accounts.rb
+++ b/lib/accounts.rb
@@ -78,30 +78,6 @@ def create_default_template(account)
new_template
end
- def load_webhook_url(account)
- load_webhook_config(account)&.value.presence
- end
-
- def load_webhook_config(account)
- configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
-
- if !configs && !Docuseal.multitenant? && !account.testing?
- configs = Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
- end
-
- configs
- end
-
- def load_webhook_preferences(account)
- configs = account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
-
- unless Docuseal.multitenant?
- configs ||= Account.order(:id).first.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
- end
-
- configs&.value.presence || {}
- end
-
def load_signing_pkcs(account)
cert_data =
if Docuseal.multitenant?
diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb
index 2a320bc8e..40e96618c 100644
--- a/lib/submitters/submit_values.rb
+++ b/lib/submitters/submit_values.rb
@@ -14,14 +14,17 @@ def call(submitter, params, request)
unless submitter.submission_events.exists?(event_type: 'start_form')
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request)
- SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
+ WebhookUrls.for_account_id(submitter.account_id, 'form.started').each do |webhook_url|
+ SendFormStartedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
+ 'webhook_url_id' => webhook_url.id)
+ end
end
update_submitter!(submitter, params, request)
submitter.submission.save!
- ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
+ ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
submitter
end
diff --git a/lib/webhook_urls.rb b/lib/webhook_urls.rb
new file mode 100644
index 000000000..482e66a70
--- /dev/null
+++ b/lib/webhook_urls.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module WebhookUrls
+ module_function
+
+ def for_account_id(account_id, events)
+ events = Array.wrap(events)
+
+ rel = WebhookUrl.where(account_id:)
+
+ event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or)
+
+ if Docuseal.multitenant?
+ rel.where(event_arel)
+ else
+ linked_account_rel =
+ AccountLinkedAccount.where(linked_account_id: account_id).where.not(account_type: :testing).select(:account_id)
+
+ webhook_urls = rel.or(WebhookUrl.where(account_id: linked_account_rel).where(event_arel))
+
+ account_urls, linked_urls = webhook_urls.partition { |w| w.account_id == account_id }
+
+ account_urls.select { |w| w.events.intersect?(events) }.presence ||
+ (account_urls.present? ? WebhookUrl.none : linked_urls)
+ end
+ end
+end
diff --git a/spec/factories/webhook_urls.rb b/spec/factories/webhook_urls.rb
new file mode 100644
index 000000000..45a44a7a3
--- /dev/null
+++ b/spec/factories/webhook_urls.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :webhook_url do
+ account
+ url { Faker::Internet.url }
+ end
+end
diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb
new file mode 100644
index 000000000..ceec4c00c
--- /dev/null
+++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb
@@ -0,0 +1,94 @@
+# 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: replace_timestamps({
+ 'event_type' => 'form.completed',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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: replace_timestamps({
+ 'event_type' => 'form.completed',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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
diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb
new file mode 100644
index 000000000..88a88906b
--- /dev/null
+++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb
@@ -0,0 +1,94 @@
+# 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: replace_timestamps({
+ 'event_type' => 'form.declined',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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: replace_timestamps({
+ 'event_type' => 'form.declined',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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
diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb
new file mode 100644
index 000000000..863ce826f
--- /dev/null
+++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb
@@ -0,0 +1,94 @@
+# 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: replace_timestamps({
+ 'event_type' => 'form.started',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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: replace_timestamps({
+ 'event_type' => 'form.started',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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
diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
new file mode 100644
index 000000000..495e62395
--- /dev/null
+++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendFormViewedWebhookRequestJob 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.viewed']) }
+
+ 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: replace_timestamps({
+ 'event_type' => 'form.viewed',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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: replace_timestamps({
+ 'event_type' => 'form.viewed',
+ 'timestamp' => Time.current,
+ 'data' => Submitters::SerializeForWebhook.call(submitter.reload)
+ }.deep_stringify_keys),
+ 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.started'])
+
+ 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
diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
new file mode 100644
index 000000000..468430f74
--- /dev/null
+++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendSubmissionArchivedWebhookRequestJob 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(:webhook_url) { create(:webhook_url, account:, events: ['submission.archived']) }
+
+ 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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.archived',
+ 'timestamp' => Time.current,
+ 'data' => submission.reload.as_json(only: %i[id archived_at])
+ }.deep_stringify_keys),
+ 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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.archived',
+ 'timestamp' => Time.current,
+ 'data' => submission.reload.as_json(only: %i[id archived_at])
+ }.deep_stringify_keys),
+ 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: ['submission.created'])
+
+ described_class.new.perform('submission_id' => submission.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('submission_id' => submission.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['submission_id']).to eq(submission.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('submission_id' => submission.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
diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
new file mode 100644
index 000000000..1101b12af
--- /dev/null
+++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendSubmissionCompletedWebhookRequestJob do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account:) }
+ let(:template) { create(:template, account:, author: user) }
+ let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) }
+ let(:webhook_url) { create(:webhook_url, account:, events: ['submission.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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.completed',
+ 'timestamp' => Time.current,
+ 'data' => Submissions::SerializeForApi.call(submission.reload)
+ }.deep_stringify_keys),
+ 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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.completed',
+ 'timestamp' => Time.current,
+ 'data' => Submissions::SerializeForApi.call(submission.reload)
+ }.deep_stringify_keys),
+ 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: ['submission.archived'])
+
+ described_class.new.perform('submission_id' => submission.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('submission_id' => submission.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['submission_id']).to eq(submission.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('submission_id' => submission.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
diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb
new file mode 100644
index 000000000..e4004dc74
--- /dev/null
+++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendSubmissionCreatedWebhookRequestJob do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account:) }
+ let(:template) { create(:template, account:, author: user) }
+ let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) }
+ let(:webhook_url) { create(:webhook_url, account:, events: ['submission.created']) }
+
+ 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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.created',
+ 'timestamp' => Time.current,
+ 'data' => Submissions::SerializeForApi.call(submission.reload)
+ }.deep_stringify_keys),
+ 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('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'submission.created',
+ 'timestamp' => Time.current,
+ 'data' => Submissions::SerializeForApi.call(submission.reload)
+ }.deep_stringify_keys),
+ 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: ['submission.completed'])
+
+ described_class.new.perform('submission_id' => submission.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('submission_id' => submission.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['submission_id']).to eq(submission.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('submission_id' => submission.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
diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb
new file mode 100644
index 000000000..532a1beab
--- /dev/null
+++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendTemplateCreatedWebhookRequestJob do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account:) }
+ let(:template) { create(:template, account:, author: user) }
+ let(:webhook_url) { create(:webhook_url, account:, events: ['template.created']) }
+
+ 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('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'template.created',
+ 'timestamp' => Time.current,
+ 'data' => Templates::SerializeForApi.call(template.reload)
+ }.deep_stringify_keys),
+ 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('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'template.created',
+ 'timestamp' => Time.current,
+ 'data' => Templates::SerializeForApi.call(template.reload)
+ }.deep_stringify_keys),
+ 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: ['template.updated'])
+
+ described_class.new.perform('template_id' => template.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('template_id' => template.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['template_id']).to eq(template.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('template_id' => template.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
diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb
new file mode 100644
index 000000000..1bef3eb2a
--- /dev/null
+++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SendTemplateUpdatedWebhookRequestJob do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account:) }
+ let(:template) { create(:template, account:, author: user) }
+ let(:webhook_url) { create(:webhook_url, account:, events: ['template.updated']) }
+
+ 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('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'template.updated',
+ 'timestamp' => Time.current,
+ 'data' => Templates::SerializeForApi.call(template.reload)
+ }.deep_stringify_keys),
+ 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('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
+
+ expect(WebMock).to have_requested(:post, webhook_url.url).with(
+ body: replace_timestamps({
+ 'event_type' => 'template.updated',
+ 'timestamp' => Time.current,
+ 'data' => Templates::SerializeForApi.call(template.reload)
+ }.deep_stringify_keys),
+ 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: ['template.created'])
+
+ described_class.new.perform('template_id' => template.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('template_id' => template.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['template_id']).to eq(template.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('template_id' => template.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
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 86a15a9f8..94f071be6 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -67,3 +67,19 @@
Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
end
end
+
+def replace_timestamps(data, replace = /.*/)
+ timestamp_fields = %w[created_at updated_at completed_at sent_at opened_at timestamp]
+
+ data.each do |key, value|
+ if timestamp_fields.include?(key) && (value.is_a?(String) || value.is_a?(Time))
+ data[key] = replace
+ elsif value.is_a?(Hash)
+ replace_timestamps(value)
+ elsif value.is_a?(Array)
+ value.each { |item| replace_timestamps(item) if item.is_a?(Hash) }
+ end
+ end
+
+ data
+end
diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb
index 1e94eb62c..a12b98f8d 100644
--- a/spec/system/webhook_settings_spec.rb
+++ b/spec/system/webhook_settings_spec.rb
@@ -8,24 +8,110 @@
before do
sign_in(user)
- visit settings_webhooks_path
end
it 'shows webhook settings page' do
+ visit settings_webhooks_path
+
expect(page).to have_content('Webhooks')
expect(page).to have_field('Webhook URL')
expect(page).to have_button('Save')
+
+ WebhookUrl::EVENTS.each do |event|
+ expect(page).to have_field(event, type: 'checkbox', disabled: true)
+ end
+ end
+
+ it 'creates the webhook' do
+ visit settings_webhooks_path
+
+ fill_in 'Webhook URL', with: 'https://example.com/webhook'
+
+ expect do
+ click_button 'Save'
+ end.to change(WebhookUrl, :count).by(1)
+
+ webhook_url = account.webhook_urls.first
+
+ expect(webhook_url.url).to eq('https://example.com/webhook')
+ end
+
+ it 'updates the webhook' do
+ webhook_url = create(:webhook_url, account:, url: 'https://example.com/webhook')
+
+ visit settings_webhooks_path
+
+ fill_in 'Webhook URL', with: 'https://example.org/webhook'
+ click_button 'Save'
+
+ webhook_url.reload
+
+ expect(webhook_url.url).to eq('https://example.org/webhook')
end
- it 'updates the webhook URL' do
- fill_in 'Webhook URL', with: 'https://example.com'
+ it 'deletes the webhook' do
+ create(:webhook_url, account:)
+
+ visit settings_webhooks_path
+
+ fill_in 'Webhook URL', with: ''
expect do
click_button 'Save'
- end.to change(EncryptedConfig, :count).by(1)
+ end.to change(WebhookUrl, :count).by(-1)
+ end
+
+ it 'updates the webhook events' do
+ webhook_url = create(:webhook_url, account:)
+
+ visit settings_webhooks_path
+
+ expect(webhook_url.events).not_to include('submission.created')
+
+ check('submission.created')
+
+ webhook_url.reload
+
+ expect(webhook_url.events).to include('submission.created')
+ end
+
+ it 'adds a secret to the webhook' do
+ webhook_url = create(:webhook_url, account:)
+
+ visit settings_webhooks_path
+
+ expect(webhook_url.secret).to eq({})
+
+ click_link 'Add Secret'
+
+ within '#modal' do
+ fill_in 'Key', with: 'X-Signature'
+ fill_in 'Value', with: 'secret-value'
+
+ click_button 'Submit'
+
+ webhook_url.reload
+
+ expect(webhook_url.secret).to eq({ 'X-Signature' => 'secret-value' })
+ end
+ end
+
+ it 'removes a secret from the webhook' do
+ webhook_url = create(:webhook_url, account:, secret: { 'X-Signature' => 'secret-value' })
+
+ visit settings_webhooks_path
+
+ click_link 'Edit Secret'
+
+ within '#modal' do
+ fill_in 'Key', with: ''
+ fill_in 'Value', with: ''
+
+ click_button 'Submit'
- encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::WEBHOOK_URL_KEY)
+ webhook_url.reload
- expect(encrypted_config.value).to eq('https://example.com')
+ expect(webhook_url.secret).to eq({})
+ end
end
end