diff --git a/app/models/account.rb b/app/models/account.rb index 72e9d5401..2367c92b2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -78,7 +78,10 @@ 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? + report_missing_webhook_config("account id=#{id}") unless Rails.env.local? + return + end webhook_urls.create!( url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), @@ -86,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 63a4851f9..ad17847e8 100644 --- a/app/models/partnership.rb +++ b/app/models/partnership.rb @@ -37,8 +37,13 @@ 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? + report_missing_webhook_config("partnership id=#{id}") unless Rails.env.local? + return + end webhook_urls.create!( url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), @@ -46,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 new file mode 100644 index 000000000..48d4bf033 --- /dev/null +++ b/config/initializers/careerplug_webhook_config.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Fail loudly (but do not block boot) if CareerPlug webhook configuration is +# 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 #{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) + 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..2172a585c --- /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 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 + + expect(Rails.logger).not_to have_received(:error) + expect(Airbrake).not_to have_received(:notify) + end + end + + context 'when not local with both vars missing' do + before do + allow(Rails.env).to receive(:local?).and_return(false) + 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 not local with only one var missing' do + before do + 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 + + 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 not local with both vars present' do + before do + 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' + )) + 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..d51edefd6 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -90,11 +90,27 @@ 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 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..977eaa1a1 100644 --- a/spec/models/partnership_spec.rb +++ b/spec/models/partnership_spec.rb @@ -36,11 +36,27 @@ 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 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