From 7e6e5a542fab68060d21cc113346189ca779d6b5 Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Thu, 25 Jun 2026 13:34:26 -0500 Subject: [PATCH 1/2] CP-14163 - Fail loudly when CareerPlug webhook config is missing What Account and Partnership creation no longer silently skips webhook setup when CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are blank. The callbacks now log a prominent error (including the account/partnership id) and report to Airbrake, then still skip creation. Added a production boot-time initializer that does the same check on startup, so a missing config is impossible to miss on deploy. Made Partnership#create_careerplug_webhook private to match Account. Why In DocuSeal prod, these env vars were blank, so the create callbacks silently no-op'd and never produced WebhookUrl records. DocuSeal therefore never sent completion webhooks to the ATS, leaving ~97% of forms (248+ in the last 60 days) stuck at "assigned" and managers never seeing the Approve / Request Changes buttons. The silent skip let this rot undetected; now a missing config is loud and visible instead. How to test bundle exec rspec spec/models/account_spec.rb spec/models/partnership_spec.rb spec/initializers/careerplug_webhook_config_spec.rb In rails console, with both vars blank, Account.create!(name: 'x') logs an error and creates no webhook; with the vars set, it creates a WebhookUrl with the full event set and the correct X-CareerPlug-Secret. Only affects accounts/partnerships created after deploy; existing data is handled by the backfill/reconciliation tickets. --- app/models/account.rb | 8 +- app/models/partnership.rb | 10 ++- .../initializers/careerplug_webhook_config.rb | 18 ++++ .../careerplug_webhook_config_spec.rb | 82 +++++++++++++++++++ spec/models/account_spec.rb | 15 ++++ spec/models/partnership_spec.rb | 15 ++++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 config/initializers/careerplug_webhook_config.rb create mode 100644 spec/initializers/careerplug_webhook_config_spec.rb diff --git a/app/models/account.rb b/app/models/account.rb index 72e9d5401..2576802a4 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -78,7 +78,13 @@ def default_template_folder private def create_careerplug_webhook - return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank? + unless ENV['CAREERPLUG_WEBHOOK_URL'].present? && ENV['CAREERPLUG_WEBHOOK_SECRET'].present? + message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ + 'skipping webhook creation for account id=%s', id:) + Rails.logger.error(message) + Airbrake.notify(message) + return + end webhook_urls.create!( url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), diff --git a/app/models/partnership.rb b/app/models/partnership.rb index 63a4851f9..8c81775a3 100644 --- a/app/models/partnership.rb +++ b/app/models/partnership.rb @@ -37,8 +37,16 @@ def default_template_folder(author) author: author) end + private + def create_careerplug_webhook - return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank? + unless ENV['CAREERPLUG_WEBHOOK_URL'].present? && ENV['CAREERPLUG_WEBHOOK_SECRET'].present? + message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ + 'skipping webhook creation for partnership id=%s', id:) + Rails.logger.error(message) + Airbrake.notify(message) + return + end webhook_urls.create!( url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), diff --git a/config/initializers/careerplug_webhook_config.rb b/config/initializers/careerplug_webhook_config.rb new file mode 100644 index 000000000..abac2e9ec --- /dev/null +++ b/config/initializers/careerplug_webhook_config.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Fail loudly (but do not block boot) if CareerPlug webhook configuration is +# missing in production. Without these env vars, the Account/Partnership +# create callbacks skip WebhookUrl creation, which leaves ATS form submissions +# stuck at "assigned" forever. A missing config should be impossible to miss +# on deploy, so this logs a prominent error and reports to error tracking. +# Non-blocking so that console/migrate access remains available for recovery. +if Rails.env.production? + missing = %w[CAREERPLUG_WEBHOOK_URL CAREERPLUG_WEBHOOK_SECRET].reject { |key| ENV[key].present? } + + unless missing.empty? + message = "CareerPlug webhook config missing in production: #{missing.join(', ')}. " \ + 'New accounts/partnerships will not get webhooks until this is fixed.' + Rails.logger.error("[careerplug_webhook_config] #{message}") + Airbrake.notify(message) + end +end diff --git a/spec/initializers/careerplug_webhook_config_spec.rb b/spec/initializers/careerplug_webhook_config_spec.rb new file mode 100644 index 000000000..03e3b95bf --- /dev/null +++ b/spec/initializers/careerplug_webhook_config_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# rubocop:disable RSpec/DescribeClass The initializer is a top-level script, not a class. +RSpec.describe 'careerplug_webhook_config initializer' do + let(:initializer_path) { Rails.root.join('config/initializers/careerplug_webhook_config.rb').to_s } + + # The initializer is a one-shot top-level script; re-evaluate it under the + # current stubs to exercise each branch. + def load_initializer + load initializer_path + end + + before do + allow(Rails.logger).to receive(:error) + allow(Airbrake).to receive(:notify) + end + + context 'when not in production' do + before { allow(Rails.env).to receive(:production?).and_return(false) } + + it 'does not log an error or report to Airbrake' do + load_initializer + + expect(Rails.logger).not_to have_received(:error) + expect(Airbrake).not_to have_received(:notify) + end + end + + context 'when in production with both vars missing' do + before do + allow(Rails.env).to receive(:production?).and_return(true) + stub_const('ENV', ENV.to_h.except('CAREERPLUG_WEBHOOK_URL', 'CAREERPLUG_WEBHOOK_SECRET')) + end + + it 'logs a prominent error naming both missing vars' do + load_initializer + + expect(Rails.logger).to have_received(:error) + .with(/CareerPlug webhook config missing.*CAREERPLUG_WEBHOOK_URL.*CAREERPLUG_WEBHOOK_SECRET/) + end + + it 'reports the missing config to Airbrake' do + load_initializer + + expect(Airbrake).to have_received(:notify).with(/CareerPlug webhook config missing/) + end + end + + context 'when in production with only one var missing' do + before do + allow(Rails.env).to receive(:production?).and_return(true) + stub_const('ENV', ENV.to_h.merge('CAREERPLUG_WEBHOOK_URL' => 'https://example.com/events') + .except('CAREERPLUG_WEBHOOK_SECRET')) + end + + it 'logs an error naming only the missing var' do + load_initializer + + expect(Rails.logger).to have_received(:error).with(/missing.*CAREERPLUG_WEBHOOK_SECRET/) + end + end + + context 'when in production with both vars present' do + before do + allow(Rails.env).to receive(:production?).and_return(true) + stub_const('ENV', ENV.to_h.merge( + 'CAREERPLUG_WEBHOOK_URL' => 'https://www.careerplug.com/api/docuseal/events', + 'CAREERPLUG_WEBHOOK_SECRET' => 'secret' + )) + end + + it 'does not log an error or report to Airbrake' do + load_initializer + + expect(Rails.logger).not_to have_received(:error) + expect(Airbrake).not_to have_received(:notify) + end + end +end +# rubocop:enable RSpec/DescribeClass diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 42e4f1eba..ce6f9f2c2 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -95,6 +95,21 @@ it 'does not create a webhook' do expect { create(:account) }.not_to change(WebhookUrl, :count) end + + it 'logs a prominent error with the account id' do + allow(Rails.logger).to receive(:error) + create(:account) + + expect(Rails.logger).to have_received(:error).with(/account id=\d+/) + end + + it 'reports the missing config to Airbrake' do + allow(Airbrake).to receive(:notify) + create(:account) + + expect(Airbrake).to have_received(:notify) + .with(%r{CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set}) + end end end diff --git a/spec/models/partnership_spec.rb b/spec/models/partnership_spec.rb index afb190201..aa4597888 100644 --- a/spec/models/partnership_spec.rb +++ b/spec/models/partnership_spec.rb @@ -41,6 +41,21 @@ it 'does not create a webhook' do expect { create(:partnership) }.not_to change(WebhookUrl, :count) end + + it 'logs a prominent error with the partnership id' do + allow(Rails.logger).to receive(:error) + create(:partnership) + + expect(Rails.logger).to have_received(:error).with(/partnership id=\d+/) + end + + it 'reports the missing config to Airbrake' do + allow(Airbrake).to receive(:notify) + create(:partnership) + + expect(Airbrake).to have_received(:notify) + .with(%r{CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set}) + end end end From 5d7f38b3a40f282ed5df80844f258e05c0cec798 Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Thu, 25 Jun 2026 14:15:42 -0500 Subject: [PATCH 2/2] CP-14163 - Fix tests --- app/models/account.rb | 12 ++++++++---- app/models/partnership.rb | 12 ++++++++---- config/initializers/careerplug_webhook_config.rb | 15 ++++++++------- .../careerplug_webhook_config_spec.rb | 16 ++++++++-------- spec/models/account_spec.rb | 1 + spec/models/partnership_spec.rb | 1 + 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index 2576802a4..2367c92b2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -79,10 +79,7 @@ def default_template_folder def create_careerplug_webhook unless ENV['CAREERPLUG_WEBHOOK_URL'].present? && ENV['CAREERPLUG_WEBHOOK_SECRET'].present? - message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ - 'skipping webhook creation for account id=%s', id:) - Rails.logger.error(message) - Airbrake.notify(message) + report_missing_webhook_config("account id=#{id}") unless Rails.env.local? return end @@ -92,4 +89,11 @@ def create_careerplug_webhook secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') } ) end + + def report_missing_webhook_config(scope) + message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ + 'skipping webhook creation for %s', scope:) + Rails.logger.error(message) + Airbrake.notify(message) + end end diff --git a/app/models/partnership.rb b/app/models/partnership.rb index 8c81775a3..ad17847e8 100644 --- a/app/models/partnership.rb +++ b/app/models/partnership.rb @@ -41,10 +41,7 @@ def default_template_folder(author) def create_careerplug_webhook unless ENV['CAREERPLUG_WEBHOOK_URL'].present? && ENV['CAREERPLUG_WEBHOOK_SECRET'].present? - message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ - 'skipping webhook creation for partnership id=%s', id:) - Rails.logger.error(message) - Airbrake.notify(message) + report_missing_webhook_config("partnership id=#{id}") unless Rails.env.local? return end @@ -54,4 +51,11 @@ def create_careerplug_webhook secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') } ) end + + def report_missing_webhook_config(scope) + message = format('CAREERPLUG_WEBHOOK_URL/CAREERPLUG_WEBHOOK_SECRET are not set; ' \ + 'skipping webhook creation for %s', scope:) + Rails.logger.error(message) + Airbrake.notify(message) + end end diff --git a/config/initializers/careerplug_webhook_config.rb b/config/initializers/careerplug_webhook_config.rb index abac2e9ec..48d4bf033 100644 --- a/config/initializers/careerplug_webhook_config.rb +++ b/config/initializers/careerplug_webhook_config.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true # Fail loudly (but do not block boot) if CareerPlug webhook configuration is -# missing in production. Without these env vars, the Account/Partnership -# create callbacks skip WebhookUrl creation, which leaves ATS form submissions -# stuck at "assigned" forever. A missing config should be impossible to miss -# on deploy, so this logs a prominent error and reports to error tracking. -# Non-blocking so that console/migrate access remains available for recovery. -if Rails.env.production? +# missing outside of local dev/test. Without these env vars, the Account/ +# Partnership create callbacks skip WebhookUrl creation, which leaves ATS form +# submissions stuck at "assigned" forever. A missing config should be impossible +# to miss on deploy, so this logs a prominent error and reports to error +# tracking. Non-blocking so that console/migrate access remains available for +# recovery. +unless Rails.env.local? missing = %w[CAREERPLUG_WEBHOOK_URL CAREERPLUG_WEBHOOK_SECRET].reject { |key| ENV[key].present? } unless missing.empty? - message = "CareerPlug webhook config missing in production: #{missing.join(', ')}. " \ + message = "CareerPlug webhook config missing in #{Rails.env}: #{missing.join(', ')}. " \ 'New accounts/partnerships will not get webhooks until this is fixed.' Rails.logger.error("[careerplug_webhook_config] #{message}") Airbrake.notify(message) diff --git a/spec/initializers/careerplug_webhook_config_spec.rb b/spec/initializers/careerplug_webhook_config_spec.rb index 03e3b95bf..2172a585c 100644 --- a/spec/initializers/careerplug_webhook_config_spec.rb +++ b/spec/initializers/careerplug_webhook_config_spec.rb @@ -17,8 +17,8 @@ def load_initializer allow(Airbrake).to receive(:notify) end - context 'when not in production' do - before { allow(Rails.env).to receive(:production?).and_return(false) } + context 'when in a local environment (development/test)' do + before { allow(Rails.env).to receive(:local?).and_return(true) } it 'does not log an error or report to Airbrake' do load_initializer @@ -28,9 +28,9 @@ def load_initializer end end - context 'when in production with both vars missing' do + context 'when not local with both vars missing' do before do - allow(Rails.env).to receive(:production?).and_return(true) + allow(Rails.env).to receive(:local?).and_return(false) stub_const('ENV', ENV.to_h.except('CAREERPLUG_WEBHOOK_URL', 'CAREERPLUG_WEBHOOK_SECRET')) end @@ -48,9 +48,9 @@ def load_initializer end end - context 'when in production with only one var missing' do + context 'when not local with only one var missing' do before do - allow(Rails.env).to receive(:production?).and_return(true) + allow(Rails.env).to receive(:local?).and_return(false) stub_const('ENV', ENV.to_h.merge('CAREERPLUG_WEBHOOK_URL' => 'https://example.com/events') .except('CAREERPLUG_WEBHOOK_SECRET')) end @@ -62,9 +62,9 @@ def load_initializer end end - context 'when in production with both vars present' do + context 'when not local with both vars present' do before do - allow(Rails.env).to receive(:production?).and_return(true) + allow(Rails.env).to receive(:local?).and_return(false) stub_const('ENV', ENV.to_h.merge( 'CAREERPLUG_WEBHOOK_URL' => 'https://www.careerplug.com/api/docuseal/events', 'CAREERPLUG_WEBHOOK_SECRET' => 'secret' diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index ce6f9f2c2..d51edefd6 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -90,6 +90,7 @@ context 'when env vars are missing' do before do stub_const('ENV', ENV.to_h.except('CAREERPLUG_WEBHOOK_URL', 'CAREERPLUG_WEBHOOK_SECRET')) + allow(Rails.env).to receive(:local?).and_return(false) end it 'does not create a webhook' do diff --git a/spec/models/partnership_spec.rb b/spec/models/partnership_spec.rb index aa4597888..977eaa1a1 100644 --- a/spec/models/partnership_spec.rb +++ b/spec/models/partnership_spec.rb @@ -36,6 +36,7 @@ context 'when env vars are missing' do before do stub_const('ENV', ENV.to_h.except('CAREERPLUG_WEBHOOK_URL', 'CAREERPLUG_WEBHOOK_SECRET')) + allow(Rails.env).to receive(:local?).and_return(false) end it 'does not create a webhook' do