From 7309581b756750605968d653b0a740e899946471 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 18 Mar 2026 08:46:19 +0200 Subject: [PATCH] add template archived event --- app/controllers/api/templates_controller.rb | 3 + app/controllers/templates_controller.rb | 2 + app/controllers/webhook_events_controller.rb | 2 +- ...d_template_archived_webhook_request_job.rb | 38 +++++++ app/models/webhook_url.rb | 1 + lib/webhook_urls.rb | 3 +- ...plate_archived_webhook_request_job_spec.rb | 100 ++++++++++++++++++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 app/jobs/send_template_archived_webhook_request_job.rb create mode 100644 spec/jobs/send_template_archived_webhook_request_job_spec.rb diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index df4bcee7..87b756ea 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -69,6 +69,7 @@ module Api SearchEntries.enqueue_reindex(@template) WebhookUrls.enqueue_events(@template, 'template.updated') + WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true render json: @template.as_json(only: %i[id updated_at]) end @@ -78,6 +79,8 @@ module Api @template.destroy! else @template.update!(archived_at: Time.current) + + WebhookUrls.enqueue_events(@template, 'template.archived') end render json: @template.as_json(only: %i[id archived_at]) diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index b9496320..e402f53a 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -84,6 +84,8 @@ class TemplatesController < ApplicationController else @template.update!(archived_at: Time.current) + WebhookUrls.enqueue_events(@template, 'template.archived') + I18n.t('template_has_been_archived') end diff --git a/app/controllers/webhook_events_controller.rb b/app/controllers/webhook_events_controller.rb index 55bcb4ea..62ef534c 100644 --- a/app/controllers/webhook_events_controller.rb +++ b/app/controllers/webhook_events_controller.rb @@ -15,7 +15,7 @@ class WebhookEventsController < ApplicationController Submissions::SerializeForApi.call(@webhook_event.record) when 'template.created', 'template.updated' Templates::SerializeForApi.call(@webhook_event.record) - when 'submission.archived' + when 'submission.archived', 'template.archived' @webhook_event.record.as_json(only: %i[id archived_at]) end end diff --git a/app/jobs/send_template_archived_webhook_request_job.rb b/app/jobs/send_template_archived_webhook_request_job.rb new file mode 100644 index 00000000..1b1c7c92 --- /dev/null +++ b/app/jobs/send_template_archived_webhook_request_job.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class SendTemplateArchivedWebhookRequestJob + include Sidekiq::Job + + sidekiq_options queue: :webhooks + + MAX_ATTEMPTS = 10 + + def perform(params = {}) + template = Template.find_by(id: params['template_id']) + + return unless template + + webhook_url = WebhookUrl.find_by(id: params['webhook_url_id']) + + return unless webhook_url + + attempt = params['attempt'].to_i + + return if webhook_url.url.blank? || webhook_url.events.exclude?('template.archived') + + resp = SendWebhookRequest.call(webhook_url, event_type: 'template.archived', + event_uuid: params['event_uuid'], + record: template, + attempt:, + data: template.as_json(only: %i[id archived_at])) + + if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && + (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) + SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { + **params, + 'attempt' => attempt + 1, + 'last_status' => resp&.status.to_i + }) + end + end +end diff --git a/app/models/webhook_url.rb b/app/models/webhook_url.rb index b0de0f58..6e92161d 100644 --- a/app/models/webhook_url.rb +++ b/app/models/webhook_url.rb @@ -34,6 +34,7 @@ class WebhookUrl < ApplicationRecord submission.archived template.created template.updated + template.archived ].freeze belongs_to :account diff --git a/lib/webhook_urls.rb b/lib/webhook_urls.rb index 50ab4785..242c274c 100644 --- a/lib/webhook_urls.rb +++ b/lib/webhook_urls.rb @@ -11,7 +11,8 @@ module WebhookUrls 'submission.expired' => SendSubmissionExpiredWebhookRequestJob, 'submission.archived' => SendSubmissionArchivedWebhookRequestJob, 'template.created' => SendTemplateCreatedWebhookRequestJob, - 'template.updated' => SendTemplateUpdatedWebhookRequestJob + 'template.updated' => SendTemplateUpdatedWebhookRequestJob, + 'template.archived' => SendTemplateArchivedWebhookRequestJob }.freeze EVENT_TYPE_ID_KEYS = { diff --git a/spec/jobs/send_template_archived_webhook_request_job_spec.rb b/spec/jobs/send_template_archived_webhook_request_job_spec.rb new file mode 100644 index 00000000..d40f68b5 --- /dev/null +++ b/spec/jobs/send_template_archived_webhook_request_job_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +RSpec.describe SendTemplateArchivedWebhookRequestJob 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.archived']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + around do |example| + freeze_time { example.run } + end + + 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, + 'event_uuid' => SecureRandom.uuid) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: { + 'event_type' => 'template.archived', + 'timestamp' => /.*/, + 'data' => template.reload.as_json(only: %i[id archived_at]) + }, + 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, + 'event_uuid' => SecureRandom.uuid) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: { + 'event_type' => 'template.archived', + 'timestamp' => /.*/, + 'data' => template.reload.as_json(only: %i[id archived_at]) + }, + 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, + 'event_uuid' => SecureRandom.uuid) + + 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) + + event_uuid = SecureRandom.uuid + + expect do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, + 'event_uuid' => event_uuid) + 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['event_uuid']).to eq(event_uuid) + 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, + 'event_uuid' => SecureRandom.uuid, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end