Files
chatwoot/spec/services/whatsapp/webhook_teardown_service_spec.rb
9328f8739c fix: clear whatsapp webhook override when manual cloud inbox is deleted (#15010)
Deleting a manually-configured WhatsApp Cloud inbox left its
phone-number-level webhook override still pointing at Chatwoot on Meta's
side. The number kept routing inbound events to us after the inbox was
gone, which blocked the customer's own app — subscribed separately on
the same WABA — from receiving messages, since the phone-level override
takes priority over the app-level subscription. Deleting the inbox now
releases the override, as it already did for embedded-signup inboxes.

## What changed

The setup and teardown paths gated on opposite halves of the same
condition. `Channel::Whatsapp#should_auto_setup_webhooks?` sets the
override for `whatsapp_cloud` inboxes where `source !=
'embedded_signup'` (i.e. manual ones), while
`Whatsapp::WebhookTeardownService#should_teardown_webhook?` only cleared
it when `source == 'embedded_signup'`. The two sets are disjoint, so
manual inboxes were exactly the ones that set an override on create and
never cleared it on destroy. Embedded-signup inboxes were unaffected
because `EmbeddedSignupService` calls `setup_webhooks` explicitly.

Dropping the `source` check from the teardown guard is the whole fix.
Manual `whatsapp_cloud` channels can't persist without `api_key`,
`phone_number_id` and `business_account_id` (`validate_provider_config`
verifies all three against Meta), so the remaining presence guards and
both API calls have everything they need. The WABA-level `DELETE
/subscribed_apps` now also fires for manual inboxes when the last one on
a WABA is removed, which is symmetric with manual setup subscribing the
app in the first place; the token only unsubscribes the app it belongs
to, so a customer's separate app subscription is untouched.

This fixes the leak going forward. Numbers already stranded still need
the override cleared with the customer's own token, since we no longer
hold their `api_key` once the inbox is deleted.

## How to reproduce

1. Create a WhatsApp Cloud inbox using manual API keys (not embedded
signup).
2. Confirm the override is set: `GET
/v22.0/{phone_number_id}?fields=webhook_configuration` shows
`phone_number` pointing at your Chatwoot install.
3. Delete the inbox.
4. Before this change, the override still points at Chatwoot. After it,
`webhook_configuration` no longer carries the phone-level override and
events fall back to the WABA/app-level subscription.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-14 15:05:27 +05:30

108 lines
3.7 KiB
Ruby

require 'rails_helper'
RSpec.describe Whatsapp::WebhookTeardownService do
describe '#perform' do
let(:channel) { create(:channel_whatsapp, validate_provider_config: false, sync_templates: false) }
let(:service) { described_class.new(channel) }
context 'when channel is whatsapp_cloud with embedded_signup' do
before do
# Stub webhook setup to prevent HTTP calls during channel update
allow(channel).to receive(:setup_webhooks).and_return(true)
channel.update!(
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'phone_number_id' => 'test_phone_id',
'api_key' => 'test_api_key'
}
)
end
it 'calls clear_phone_number_callback_override on Facebook API client' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).with('test_api_key').and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override).with('test_phone_id')
service.perform
expect(api_client).to have_received(:clear_phone_number_callback_override).with('test_phone_id')
end
it 'handles errors gracefully without raising' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override).and_raise(StandardError, 'API Error')
expect { service.perform }.not_to raise_error
end
end
context 'when channel is not whatsapp_cloud' do
before do
channel.update!(provider: 'default')
end
it 'does not attempt to unsubscribe webhook' do
expect(Whatsapp::FacebookApiClient).not_to receive(:new)
service.perform
end
end
context 'when channel is whatsapp_cloud with manual setup' do
before do
allow(channel).to receive(:setup_webhooks).and_return(true)
channel.update!(
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'manual',
'phone_number_id' => 'manual_phone_id',
'business_account_id' => 'manual_waba_id',
'api_key' => 'manual_api_key'
}
)
end
it 'clears the phone number callback override' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).with('manual_api_key').and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override).with('manual_phone_id')
service.perform
expect(api_client).to have_received(:clear_phone_number_callback_override).with('manual_phone_id')
end
# The manual token belongs to the customer's own Meta app, so its WABA subscription is not ours to remove.
it 'does not unsubscribe the app from the WABA' do
api_client = instance_double(Whatsapp::FacebookApiClient)
allow(Whatsapp::FacebookApiClient).to receive(:new).and_return(api_client)
allow(api_client).to receive(:clear_phone_number_callback_override)
allow(api_client).to receive(:unsubscribe_app_from_waba)
service.perform
expect(api_client).not_to have_received(:unsubscribe_app_from_waba)
end
end
context 'when required config is missing' do
before do
channel.update!(
provider: 'whatsapp_cloud',
provider_config: { 'source' => 'embedded_signup' }
)
end
it 'does not attempt to unsubscribe webhook' do
expect(Whatsapp::FacebookApiClient).not_to receive(:new)
service.perform
end
end
end
end