mirror of https://github.com/docusealco/docuseal
				
				
				
			
							parent
							
								
									f669045362
								
							
						
					
					
						commit
						84edb6dbda
					
				| @ -1,37 +0,0 @@ | |||||||
| # 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 |  | ||||||
| 
 |  | ||||||
|   before_action :load_account_config |  | ||||||
|   authorize_resource :account_config, parent: false |  | ||||||
| 
 |  | ||||||
|   def create |  | ||||||
|     @account_config.value[account_config_params[:event]] = account_config_params[:value] == '1' |  | ||||||
| 
 |  | ||||||
|     @account_config.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) |  | ||||||
|   end |  | ||||||
| end |  | ||||||
| @ -1,29 +1,21 @@ | |||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| class WebhookSecretController < ApplicationController | class WebhookSecretController < ApplicationController | ||||||
|   before_action :load_encrypted_config |   load_and_authorize_resource :webhook_url, parent: false | ||||||
|   authorize_resource :encrypted_config, parent: false |  | ||||||
| 
 | 
 | ||||||
|   def index; end |   def show; end | ||||||
| 
 | 
 | ||||||
|   def create |   def update | ||||||
|     @encrypted_config.assign_attributes(value: { |     @webhook_url.update!(secret: { | ||||||
|       encrypted_config_params[:key] => encrypted_config_params[:value] |       webhook_secret_params[:key] => webhook_secret_params[:value] | ||||||
|     }.compact_blank) |     }.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')) |     redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved')) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def load_encrypted_config |   def webhook_secret_params | ||||||
|     @encrypted_config = |     params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {}) | ||||||
|       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, {}) |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| <%= render 'shared/turbo_modal', title: 'Webhook Secret' do %> | <%= render 'shared/turbo_modal', title: t('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| %> |   <%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %> | ||||||
|     <div class="space-y-2"> |     <div class="space-y-2"> | ||||||
|       <%= 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| %> | ||||||
|         <div class="form-control"> |         <div class="form-control"> | ||||||
|           <%= ff.label :key, class: 'label' %> |           <%= ff.label :key, t('key'), class: 'label' %> | ||||||
|           <%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %> |           <%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %> | ||||||
|         </div> |         </div> | ||||||
|         <div class="form-control"> |         <div class="form-control"> | ||||||
|           <%= ff.label :value, class: 'label' %> |           <%= ff.label :value, t('value'), class: 'label' %> | ||||||
|           <%= ff.text_field :value, class: 'base-input' %> |           <%= ff.text_field :value, class: 'base-input' %> | ||||||
|         </div> |         </div> | ||||||
|       <% end %> |       <% end %> | ||||||
| @ -0,0 +1,7 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2] | ||||||
|  |   def change | ||||||
|  |     add_column :webhook_urls, :secret, :text | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,50 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class PopulateWebhookUrls < ActiveRecord::Migration[7.2] | ||||||
|  |   disable_ddl_transaction | ||||||
|  | 
 | ||||||
|  |   class MigrationWebhookUrl < ApplicationRecord | ||||||
|  |     self.table_name = 'webhook_urls' | ||||||
|  | 
 | ||||||
|  |     serialize :events, coder: JSON | ||||||
|  |     serialize :secret, coder: JSON | ||||||
|  |     encrypts :url, :secret | ||||||
|  | 
 | ||||||
|  |     before_validation -> { self.sha1 = Digest::SHA1.hexdigest(url) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   class MigrationEncryptedConfig < ApplicationRecord | ||||||
|  |     self.table_name = 'encrypted_configs' | ||||||
|  | 
 | ||||||
|  |     encrypts :value | ||||||
|  |     serialize :value, coder: JSON | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   class MigrationAccountConfig < ApplicationRecord | ||||||
|  |     self.table_name = 'account_configs' | ||||||
|  | 
 | ||||||
|  |     serialize :value, coder: JSON | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     MigrationEncryptedConfig.joins('INNER JOIN accounts a ON a.id = encrypted_configs.account_id') | ||||||
|  |                             .where(key: 'webhook_url') | ||||||
|  |                             .find_each do |config| | ||||||
|  |       webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id, url: config.value) | ||||||
|  |       webhook_url.secret = MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value | ||||||
|  | 
 | ||||||
|  |       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.save! | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     nil | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,8 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | FactoryBot.define do | ||||||
|  |   factory :webhook_url do | ||||||
|  |     account | ||||||
|  |     url { Faker::Internet.url } | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,108 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submitter doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,108 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submitter doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,108 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submitter doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,108 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submitter doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,106 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submission doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,106 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submission doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,106 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the submission doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,104 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the template doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
| @ -0,0 +1,104 @@ | |||||||
|  | # 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.co 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.co Webhook', | ||||||
|  |           'X-Secret-Header' => 'secret_value' | ||||||
|  |         } | ||||||
|  |       ).once | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't send a webhook request if the template doesn't exist" do | ||||||
|  |       expect do | ||||||
|  |         described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) | ||||||
|  |       end.to raise_error ActiveRecord::RecordNotFound | ||||||
|  | 
 | ||||||
|  |       expect(WebMock).not_to have_requested(:post, webhook_url.url) | ||||||
|  |     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 "doesn't send a webhook request if the webhook doesn't exist" do | ||||||
|  |       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
					Loading…
					
					
				
		Reference in new issue