feat(whatsapp): add cloud template management token (#15218)
Chatwoot Cloud customers can now provide a dedicated WhatsApp business management token when their Embedded Signup credential cannot access message templates. Once validated, the token is stored securely and used only for template synchronization. Existing inboxes continue using their configured WhatsApp API key when no business management token is present. Sending, receiving, webhooks, phone-number health, and other WhatsApp operations remain unchanged. ### Things to know - This option is available only on Chatwoot Cloud. - Saving the token verifies that `whatsapp_business_management` is granted through Meta's permissions endpoint; template synchronization still verifies access to the configured WhatsApp Business Account. - The token is encrypted using the existing external-credentials encryption mechanism. - Self-hosted installations continue using the existing API key flow. ### How to test 1. On Chatwoot Cloud, open a WhatsApp Cloud inbox and go to **Configuration**. 2. Enter a token with `whatsapp_business_management` access and save it. 3. Confirm the token is accepted and the value is not exposed again in the UI or API. 4. Select **Sync Templates** and confirm templates are fetched with the saved business management token. 5. Remove the token and confirm template synchronization falls back to the inbox API key. 6. Confirm the business management token controls are not shown on a self-hosted installation. ### What changed - Added an encrypted `business_management_token` credential to WhatsApp channels. - Added Cloud-only endpoints and UI controls to validate the required permission, save, and remove the token. - Added template-sync credential selection with API-key fallback. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This commit is contained in:
@@ -44,8 +44,23 @@ module Api::V1::Accounts::Concerns::WhatsappHealthManagement
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def whatsapp_business_management_token
|
||||
Whatsapp::BusinessManagementTokenService.new(whatsapp_channel).update!(params.require(:business_management_token))
|
||||
|
||||
head :no_content
|
||||
rescue ArgumentError, ActiveRecord::RecordInvalid => e
|
||||
render json: { error: e.message, message: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def whatsapp_channel
|
||||
channel = @inbox.channel
|
||||
raise ActiveRecord::RecordNotFound unless channel.is_a?(Channel::Whatsapp)
|
||||
|
||||
channel
|
||||
end
|
||||
|
||||
def validate_whatsapp_cloud_channel
|
||||
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ class Inboxes extends CacheEnabledApiClient {
|
||||
return axios.post(`${this.url}/${inboxId}/sync_templates`);
|
||||
}
|
||||
|
||||
updateWhatsappBusinessManagementToken(inboxId, businessManagementToken) {
|
||||
return axios.put(
|
||||
`${this.url}/${inboxId}/whatsapp_business_management_token`,
|
||||
{
|
||||
business_management_token: businessManagementToken,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createCSATTemplate(inboxId, template) {
|
||||
return axios.post(`${this.url}/${inboxId}/csat_template`, {
|
||||
template,
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('#InboxesAPI', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
put: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
@@ -48,5 +49,13 @@ describe('#InboxesAPI', () => {
|
||||
'/api/v1/inboxes/2/sync_templates'
|
||||
);
|
||||
});
|
||||
|
||||
it('#updateWhatsappBusinessManagementToken', () => {
|
||||
inboxesAPI.updateWhatsappBusinessManagementToken(2, 'business-token');
|
||||
expect(axiosMock.put).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/whatsapp_business_management_token',
|
||||
{ business_management_token: 'business-token' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -894,6 +894,16 @@
|
||||
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
|
||||
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_ADD_TITLE": "Add Business Management Token",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_REPLACE_TITLE": "Replace Business Management Token",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_CONFIGURED": "Business management token configured",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_SUBHEADER": "Provide a token with access to this WhatsApp Business Account when Chatwoot cannot retrieve its message templates. Chatwoot uses this token only for template synchronization.",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_PLACEHOLDER": "Enter the business management token",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_ADD_BUTTON": "Add",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_REPLACE_BUTTON": "Replace",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_SUCCESS": "WhatsApp business management token updated successfully.",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_ERROR": "Could not update the business management token. Verify its WhatsApp Business Management permission and account access, then try again.",
|
||||
"WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_GUIDE_LINK": "Learn how to generate a WhatsApp Business Management token",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
|
||||
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { required } from '@vuelidate/validators';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'next/textarea/TextArea.vue';
|
||||
import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper';
|
||||
import WhatsappBusinessManagementToken from './WhatsappBusinessManagementToken.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -25,6 +26,7 @@ export default {
|
||||
SmtpSettings,
|
||||
NextButton,
|
||||
TextArea,
|
||||
WhatsappBusinessManagementToken,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -469,6 +471,14 @@ export default {
|
||||
</div>
|
||||
</SettingsFieldSection>
|
||||
</template>
|
||||
<WhatsappBusinessManagementToken
|
||||
v-if="
|
||||
isOnChatwootCloud &&
|
||||
inbox.provider === 'whatsapp_cloud' &&
|
||||
isEmbeddedSignupWhatsApp
|
||||
"
|
||||
:inbox="inbox"
|
||||
/>
|
||||
<SettingsFieldSection
|
||||
:label="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_TEMPLATES_SYNC_TITLE')"
|
||||
:help-text="
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import InboxesAPI from 'dashboard/api/inboxes';
|
||||
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_GUIDE_URL = 'https://chwt.app/zM7G2yU';
|
||||
const businessManagementToken = ref('');
|
||||
const isUpdating = ref(false);
|
||||
const tokenUpdated = ref(false);
|
||||
const isTokenConfigured = computed(
|
||||
() =>
|
||||
tokenUpdated.value ||
|
||||
Boolean(props.inbox.business_management_token_configured)
|
||||
);
|
||||
const isUpdateDisabled = computed(
|
||||
() => !businessManagementToken.value || isUpdating.value
|
||||
);
|
||||
const sectionTitle = computed(() =>
|
||||
isTokenConfigured.value
|
||||
? t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_REPLACE_TITLE'
|
||||
)
|
||||
: t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_ADD_TITLE'
|
||||
)
|
||||
);
|
||||
const actionLabel = computed(() =>
|
||||
isTokenConfigured.value
|
||||
? t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_REPLACE_BUTTON'
|
||||
)
|
||||
: t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_ADD_BUTTON'
|
||||
)
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.inbox.id,
|
||||
() => {
|
||||
businessManagementToken.value = '';
|
||||
tokenUpdated.value = false;
|
||||
isUpdating.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
const updateToken = async () => {
|
||||
const inboxId = props.inbox.id;
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
await InboxesAPI.updateWhatsappBusinessManagementToken(
|
||||
inboxId,
|
||||
businessManagementToken.value
|
||||
);
|
||||
if (props.inbox.id !== inboxId) return;
|
||||
|
||||
businessManagementToken.value = '';
|
||||
tokenUpdated.value = true;
|
||||
useAlert(
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_SUCCESS'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (props.inbox.id !== inboxId) return;
|
||||
|
||||
useAlert(
|
||||
error.response?.data?.message ||
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_ERROR'
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
if (props.inbox.id === inboxId) {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsFieldSection
|
||||
:label="sectionTitle"
|
||||
:help-text="
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_SUBHEADER'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="isTokenConfigured"
|
||||
class="inline-flex w-fit items-center gap-1.5 rounded-md bg-n-alpha-2 px-2 py-1 text-label-small text-n-teal-11"
|
||||
>
|
||||
<span class="size-1.5 rounded-full bg-n-teal-9" />
|
||||
{{
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_CONFIGURED'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-1 justify-between items-center whatsapp-settings--content"
|
||||
>
|
||||
<woot-input
|
||||
v-model="businessManagementToken"
|
||||
type="password"
|
||||
class="flex-1 mr-2 [&>input]:!mb-0"
|
||||
:placeholder="
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_UPDATE_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<NextButton
|
||||
:disabled="isUpdateDisabled"
|
||||
:is-loading="isUpdating"
|
||||
@click="updateToken"
|
||||
>
|
||||
{{ actionLabel }}
|
||||
</NextButton>
|
||||
</div>
|
||||
<a
|
||||
:href="WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_GUIDE_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-label-small text-n-blue-11 hover:underline"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_BUSINESS_MANAGEMENT_TOKEN_GUIDE_LINK'
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</div>
|
||||
</SettingsFieldSection>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@
|
||||
# Table name: channel_whatsapp
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# business_management_token :text
|
||||
# message_templates :jsonb
|
||||
# message_templates_last_updated :datetime
|
||||
# phone_number :string not null
|
||||
@@ -27,6 +28,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
|
||||
self.table_name = 'channel_whatsapp'
|
||||
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
||||
encrypts :business_management_token if Chatwoot.encryption_configured?
|
||||
|
||||
# default at the moment is 360dialog lets change later.
|
||||
PROVIDERS = %w[default whatsapp_cloud].freeze
|
||||
@@ -74,6 +76,16 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def template_access_token
|
||||
return provider_config['api_key'] unless ChatwootApp.chatwoot_cloud? && provider_config['source'] == 'embedded_signup'
|
||||
|
||||
business_management_token.presence || provider_config['api_key']
|
||||
end
|
||||
|
||||
def serializable_hash(options = nil)
|
||||
super.except('business_management_token')
|
||||
end
|
||||
|
||||
# Enables voice: turns calling on at Meta (idempotent), then re-registers webhooks
|
||||
# with the in-memory calling_enabled flag so the `calls` field is subscribed. The
|
||||
# flag is persisted only after registration succeeds, so a webhook failure can't
|
||||
|
||||
@@ -62,6 +62,10 @@ class InboxPolicy < ApplicationPolicy
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def whatsapp_business_management_token?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def health?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
27
app/services/whatsapp/business_management_token_service.rb
Normal file
27
app/services/whatsapp/business_management_token_service.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class Whatsapp::BusinessManagementTokenService
|
||||
def initialize(channel)
|
||||
@channel = channel
|
||||
end
|
||||
|
||||
def update!(business_management_token)
|
||||
validate_channel!
|
||||
raise ArgumentError, 'Business management token is required' if business_management_token.blank?
|
||||
|
||||
Whatsapp::BusinessManagementTokenValidationService.new(
|
||||
business_management_token,
|
||||
@channel.provider_config['business_account_id']
|
||||
).perform
|
||||
|
||||
@channel.business_management_token = business_management_token
|
||||
@channel.save!(validate: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_channel!
|
||||
raise ArgumentError, 'Business management token is only available on Chatwoot Cloud' unless ChatwootApp.chatwoot_cloud?
|
||||
return if @channel.provider == 'whatsapp_cloud' && @channel.provider_config['source'] == 'embedded_signup'
|
||||
|
||||
raise ArgumentError, 'Business management token is only supported for WhatsApp Embedded Signup inboxes'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
class Whatsapp::BusinessManagementTokenValidationService
|
||||
REQUIRED_PERMISSION = 'whatsapp_business_management'.freeze
|
||||
|
||||
def initialize(business_management_token, business_account_id)
|
||||
@business_management_token = business_management_token
|
||||
@business_account_id = business_account_id
|
||||
end
|
||||
|
||||
def perform
|
||||
response = HTTParty.get(permissions_url, headers: { 'Authorization' => "Bearer #{@business_management_token}" })
|
||||
|
||||
raise ArgumentError, response_error(response) unless response.success?
|
||||
raise ArgumentError, "Business management token must grant the #{REQUIRED_PERMISSION} permission" unless required_permission_granted?(response)
|
||||
|
||||
validate_business_account_access!
|
||||
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError
|
||||
raise ArgumentError, 'Could not validate business management token permissions. Please try again.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permissions_url
|
||||
base_path = ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com')
|
||||
api_version = GlobalConfigService.load('WHATSAPP_API_VERSION', 'v22.0')
|
||||
"#{base_path}/#{api_version}/me/permissions"
|
||||
end
|
||||
|
||||
def business_account_templates_url
|
||||
base_path = ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com')
|
||||
api_version = GlobalConfigService.load('WHATSAPP_API_VERSION', 'v22.0')
|
||||
"#{base_path}/#{api_version}/#{@business_account_id}/message_templates?limit=1"
|
||||
end
|
||||
|
||||
def required_permission_granted?(response)
|
||||
permissions = response.parsed_response.is_a?(Hash) ? response.parsed_response['data'] : []
|
||||
Array(permissions).select { |permission| permission.is_a?(Hash) }
|
||||
.any? { |permission| permission['permission'] == REQUIRED_PERMISSION && permission['status'] == 'granted' }
|
||||
end
|
||||
|
||||
def validate_business_account_access!
|
||||
response = HTTParty.get(
|
||||
business_account_templates_url,
|
||||
headers: { 'Authorization' => "Bearer #{@business_management_token}" }
|
||||
)
|
||||
|
||||
raise ArgumentError, response_error(response) unless response.success?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def response_error(response)
|
||||
error_message = response.parsed_response.is_a?(Hash) ? response.parsed_response.dig('error', 'message') : nil
|
||||
error_message.presence || 'Could not validate business management token permissions'
|
||||
end
|
||||
end
|
||||
@@ -35,29 +35,29 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
def sync_templates
|
||||
# ensuring that channels with wrong provider config wouldn't keep trying to sync templates
|
||||
whatsapp_channel.mark_message_templates_updated
|
||||
templates = fetch_whatsapp_templates("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
|
||||
whatsapp_channel.update(message_templates: templates, message_templates_last_updated: Time.now.utc) if templates.present?
|
||||
templates = fetch_whatsapp_templates
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
whatsapp_channel.update_columns(message_templates: templates, message_templates_last_updated: Time.current) if templates.present?
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def fetch_whatsapp_templates(url)
|
||||
response = HTTParty.get(url)
|
||||
def fetch_whatsapp_templates(after: nil)
|
||||
options = { headers: { 'Authorization' => "Bearer #{whatsapp_channel.template_access_token}" } }
|
||||
options[:query] = { after: after } if after.present?
|
||||
response = HTTParty.get("#{business_account_path}/message_templates", options)
|
||||
unless response.success?
|
||||
Rails.logger.warn "[WHATSAPP] Template sync failed for account #{whatsapp_channel.account_id} " \
|
||||
"inbox #{whatsapp_channel.inbox&.id}: #{response.code} #{error_message(response)}"
|
||||
return []
|
||||
end
|
||||
|
||||
next_url = next_url(response)
|
||||
next_cursor = response.dig('paging', 'cursors', 'after')
|
||||
|
||||
return response['data'] + fetch_whatsapp_templates(next_url) if next_url.present?
|
||||
return response['data'] + fetch_whatsapp_templates(after: next_cursor) if next_cursor.present?
|
||||
|
||||
response['data']
|
||||
end
|
||||
|
||||
def next_url(response)
|
||||
response['paging'] ? response['paging']['next'] : ''
|
||||
end
|
||||
|
||||
def validate_provider_config?
|
||||
config = whatsapp_channel.provider_config
|
||||
response = HTTParty.get("#{business_account_path}/message_templates?access_token=#{config['api_key']}")
|
||||
|
||||
@@ -29,6 +29,7 @@ class Whatsapp::ReauthorizationService
|
||||
current_config = channel.provider_config || {}
|
||||
# Legacy clients may omit phone_number_id; fall back to the value just fetched from Meta.
|
||||
resolved_phone_number_id = @phone_number_id.presence || phone_info[:phone_number_id]
|
||||
channel.business_management_token = nil if current_config['business_account_id'] != @waba_id
|
||||
|
||||
channel.provider_config = current_config.merge(
|
||||
'api_key' => access_token,
|
||||
|
||||
@@ -134,6 +134,11 @@ json.bot_name resource.channel.try(:bot_name) if resource.telegram?
|
||||
if resource.whatsapp?
|
||||
json.message_templates resource.channel.try(:message_templates)
|
||||
json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator?
|
||||
if Current.account_user&.administrator? &&
|
||||
ChatwootApp.chatwoot_cloud? &&
|
||||
(resource.channel.try(:provider_config) || {}).to_h['source'] == 'embedded_signup'
|
||||
json.business_management_token_configured resource.channel.try(:business_management_token).present?
|
||||
end
|
||||
# Only show reauthorization for embedded signup; manual flow uses API keys, not OAuth
|
||||
json.reauthorization_required(
|
||||
(resource.channel.try(:provider_config) || {}).to_h['source'] == 'embedded_signup' &&
|
||||
|
||||
@@ -283,6 +283,7 @@ Rails.application.routes.draw do
|
||||
post :set_agent_bot, on: :member
|
||||
delete :avatar, on: :member
|
||||
post :sync_templates, on: :member
|
||||
put :whatsapp_business_management_token, on: :member
|
||||
get :health, on: :member
|
||||
post :register_webhook, on: :member
|
||||
post :reset_secret, on: :member
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddBusinessManagementTokenToChannelWhatsapp < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :channel_whatsapp, :business_management_token, :text
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_07_24_000100) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_07_28_000001) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -676,6 +676,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_07_24_000100) do
|
||||
|
||||
create_table "channel_whatsapp", force: :cascade do |t|
|
||||
t.integer "account_id", null: false
|
||||
t.text "business_management_token"
|
||||
t.string "phone_number", null: false
|
||||
t.string "provider", default: "default"
|
||||
t.jsonb "provider_config", default: {}
|
||||
|
||||
@@ -126,6 +126,28 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(response.parsed_body['reauthorization_required']).to be(true)
|
||||
end
|
||||
|
||||
it 'returns only the configured state for an embedded signup WhatsApp business management token' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
whatsapp_channel = create(
|
||||
:channel_whatsapp,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
business_management_token: 'business-token',
|
||||
sync_templates: false,
|
||||
validate_provider_config: false
|
||||
)
|
||||
whatsapp_inbox = create(:inbox, channel: whatsapp_channel, account: account)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['business_management_token_configured']).to be(true)
|
||||
expect(response.parsed_body).not_to have_key('business_management_token')
|
||||
expect(response.body).not_to include('business-token')
|
||||
end
|
||||
|
||||
it 'does not flag reauthorization_required for manual whatsapp channel even when reauth required' do
|
||||
whatsapp_channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'WhatsApp business management token API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:channel) do
|
||||
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
|
||||
end
|
||||
let(:inbox) { channel.inbox }
|
||||
let(:service) { instance_double(Whatsapp::BusinessManagementTokenService, update!: true) }
|
||||
|
||||
before do
|
||||
allow(Whatsapp::BusinessManagementTokenService).to receive(:new).with(channel).and_return(service)
|
||||
end
|
||||
|
||||
it 'allows an administrator to validate and update the token' do
|
||||
put "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/whatsapp_business_management_token",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { business_management_token: 'business-token' },
|
||||
as: :json
|
||||
|
||||
expect(service).to have_received(:update!).with('business-token')
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it 'does not allow an agent to update the token' do
|
||||
put "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/whatsapp_business_management_token",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { business_management_token: 'business-token' },
|
||||
as: :json
|
||||
|
||||
expect(service).not_to have_received(:update!)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns a consistent validation error response' do
|
||||
allow(service).to receive(:update!).and_raise(ArgumentError, 'Invalid token')
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/whatsapp_business_management_token",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { business_management_token: 'business-token' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body).to eq('error' => 'Invalid token', 'message' => 'Invalid token')
|
||||
end
|
||||
end
|
||||
@@ -38,6 +38,24 @@ RSpec.describe ApplicationRecord do
|
||||
attribute: :access_token,
|
||||
value: 'ig-secret'
|
||||
|
||||
it 'encrypts WhatsApp business_management_token at rest' do
|
||||
skip('encryption keys missing; see run_mfa_spec workflow') unless Chatwoot.encryption_configured?
|
||||
|
||||
channel = create(
|
||||
:channel_whatsapp,
|
||||
provider: 'whatsapp_cloud',
|
||||
business_management_token: 'whatsapp-business-management-secret',
|
||||
validate_provider_config: false,
|
||||
sync_templates: false
|
||||
)
|
||||
|
||||
raw_stored_value = channel.reload.read_attribute_before_type_cast(:business_management_token).to_s
|
||||
expect(raw_stored_value).to be_present
|
||||
expect(raw_stored_value).not_to include('whatsapp-business-management-secret')
|
||||
expect(channel.business_management_token).to eq('whatsapp-business-management-secret')
|
||||
expect(channel.encrypted_attribute?(:business_management_token)).to be(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'encrypted external credential',
|
||||
factory: :channel_line,
|
||||
attribute: :line_channel_secret,
|
||||
|
||||
@@ -4,6 +4,65 @@ require 'rails_helper'
|
||||
require Rails.root.join 'spec/models/concerns/reauthorizable_shared.rb'
|
||||
|
||||
RSpec.describe Channel::Whatsapp do
|
||||
describe '#serializable_hash' do
|
||||
it 'does not expose the business management token' do
|
||||
channel = build(:channel_whatsapp, business_management_token: 'business-token')
|
||||
|
||||
expect(channel.serializable_hash).not_to have_key('business_management_token')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#template_access_token' do
|
||||
let(:channel) do
|
||||
build(
|
||||
:channel_whatsapp,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: { 'api_key' => 'api-key', 'source' => source },
|
||||
business_management_token: business_management_token
|
||||
)
|
||||
end
|
||||
let(:source) { 'embedded_signup' }
|
||||
|
||||
context 'when running on Chatwoot Cloud' do
|
||||
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) }
|
||||
|
||||
context 'with a business management token' do
|
||||
let(:business_management_token) { 'business-token' }
|
||||
|
||||
it 'uses the business management token' do
|
||||
expect(channel.template_access_token).to eq('business-token')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a business management token' do
|
||||
let(:business_management_token) { nil }
|
||||
|
||||
it 'uses the provider API key' do
|
||||
expect(channel.template_access_token).to eq('api-key')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a manually configured inbox' do
|
||||
let(:business_management_token) { 'business-token' }
|
||||
let(:source) { nil }
|
||||
|
||||
it 'ignores the business management token' do
|
||||
expect(channel.template_access_token).to eq('api-key')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when running outside Chatwoot Cloud' do
|
||||
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false) }
|
||||
|
||||
let(:business_management_token) { 'business-token' }
|
||||
|
||||
it 'ignores the business management token' do
|
||||
expect(channel.template_access_token).to eq('api-key')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'concerns' do
|
||||
let(:channel) { create(:channel_whatsapp) }
|
||||
|
||||
@@ -44,6 +103,9 @@ RSpec.describe Channel::Whatsapp do
|
||||
}] }.to_json)
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0//phone_numbers?fields=id&limit=100&access_token=test_key')
|
||||
.to_return(status: 200, body: { data: [{ id: 'random_id' }] }.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0//message_templates')
|
||||
.with(headers: { 'Authorization' => 'Bearer test_key' })
|
||||
.to_return(status: 200, body: { data: [] }.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
expect(channel.save).to be(true)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Whatsapp::BusinessManagementTokenService do
|
||||
let(:channel) do
|
||||
create(
|
||||
:channel_whatsapp,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'api_key' => 'integration-token',
|
||||
'phone_number_id' => 'phone-id',
|
||||
'business_account_id' => 'waba-id',
|
||||
'source' => 'embedded_signup'
|
||||
},
|
||||
sync_templates: false,
|
||||
validate_provider_config: false
|
||||
)
|
||||
end
|
||||
let(:service) { described_class.new(channel) }
|
||||
let(:validation_service) { instance_double(Whatsapp::BusinessManagementTokenValidationService, perform: true) }
|
||||
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
allow(Whatsapp::BusinessManagementTokenValidationService).to receive(:new)
|
||||
.with('business-token', channel.provider_config['business_account_id'])
|
||||
.and_return(validation_service)
|
||||
end
|
||||
|
||||
it 'stores the business management token without replacing the API key' do
|
||||
original_api_key = channel.provider_config['api_key']
|
||||
|
||||
service.update!('business-token')
|
||||
|
||||
expect(channel.reload.business_management_token).to eq('business-token')
|
||||
expect(channel.provider_config['api_key']).to eq(original_api_key)
|
||||
end
|
||||
|
||||
it 'stores the token without revalidating the existing provider API key' do
|
||||
expect(channel).not_to receive(:validate_provider_config)
|
||||
|
||||
service.update!('business-token')
|
||||
end
|
||||
|
||||
it 'rejects updates outside Chatwoot Cloud' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
|
||||
expect { service.update!('business-token') }
|
||||
.to raise_error(ArgumentError, 'Business management token is only available on Chatwoot Cloud')
|
||||
expect(validation_service).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'rejects updates for manually configured WhatsApp Cloud inboxes' do
|
||||
channel.provider_config.delete('source')
|
||||
|
||||
expect { service.update!('business-token') }
|
||||
.to raise_error(ArgumentError, 'Business management token is only supported for WhatsApp Embedded Signup inboxes')
|
||||
expect(validation_service).not_to have_received(:perform)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Whatsapp::BusinessManagementTokenValidationService do
|
||||
let(:permissions_url) { 'https://graph.facebook.com/v22.0/me/permissions' }
|
||||
let(:templates_url) { 'https://graph.facebook.com/v22.0/waba-id/message_templates?limit=1' }
|
||||
let(:service) { described_class.new('business-token', 'waba-id') }
|
||||
|
||||
around do |example|
|
||||
with_modified_env WHATSAPP_CLOUD_BASE_URL: 'https://graph.facebook.com' do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_API_VERSION', 'v22.0').and_return('v22.0')
|
||||
end
|
||||
|
||||
it 'accepts a token with the WhatsApp business management permission' do
|
||||
stub_request(:get, permissions_url)
|
||||
.with(headers: { 'Authorization' => 'Bearer business-token' })
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: {
|
||||
data: [
|
||||
{ permission: 'whatsapp_business_management', status: 'granted' },
|
||||
{ permission: 'whatsapp_business_messaging', status: 'granted' }
|
||||
]
|
||||
}.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
stub_request(:get, templates_url)
|
||||
.with(headers: { 'Authorization' => 'Bearer business-token' })
|
||||
.to_return(status: 200, body: { data: [] }.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
expect(service.perform).to be(true)
|
||||
end
|
||||
|
||||
it 'rejects a token without the WhatsApp business management permission' do
|
||||
stub_request(:get, permissions_url)
|
||||
.with(headers: { 'Authorization' => 'Bearer business-token' })
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: {
|
||||
data: [
|
||||
{ permission: 'whatsapp_business_management', status: 'declined' },
|
||||
{ permission: 'whatsapp_business_messaging', status: 'granted' }
|
||||
]
|
||||
}.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
expect { service.perform }
|
||||
.to raise_error(ArgumentError, 'Business management token must grant the whatsapp_business_management permission')
|
||||
end
|
||||
|
||||
it 'rejects malformed permission entries as a controlled validation error' do
|
||||
stub_request(:get, permissions_url)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { data: [nil, 'invalid'] }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
expect { service.perform }
|
||||
.to raise_error(ArgumentError, 'Business management token must grant the whatsapp_business_management permission')
|
||||
end
|
||||
|
||||
it 'rejects a token without access to the inbox WhatsApp Business Account' do
|
||||
stub_request(:get, permissions_url)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { data: [{ permission: 'whatsapp_business_management', status: 'granted' }] }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
stub_request(:get, templates_url)
|
||||
.with(headers: { 'Authorization' => 'Bearer business-token' })
|
||||
.to_return(
|
||||
status: 403,
|
||||
body: { error: { message: 'Permission denied' } }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
expect { service.perform }.to raise_error(ArgumentError, 'Permission denied')
|
||||
end
|
||||
|
||||
it 'returns the provider error when the permission check fails' do
|
||||
stub_request(:get, permissions_url)
|
||||
.with(headers: { 'Authorization' => 'Bearer business-token' })
|
||||
.to_return(
|
||||
status: 403,
|
||||
body: { error: { message: 'Permission denied' } }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
expect { service.perform }
|
||||
.to raise_error(ArgumentError, 'Permission denied')
|
||||
end
|
||||
|
||||
it 'returns a safe error when the permission request times out' do
|
||||
stub_request(:get, permissions_url).to_timeout
|
||||
|
||||
expect { service.perform }
|
||||
.to raise_error(ArgumentError, 'Could not validate business management token permissions. Please try again.')
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,16 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
||||
|
||||
let(:conversation) { create(:conversation, inbox: whatsapp_channel.inbox) }
|
||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) }
|
||||
let(:business_management_token) { nil }
|
||||
let(:whatsapp_channel) do
|
||||
create(
|
||||
:channel_whatsapp,
|
||||
provider: 'whatsapp_cloud',
|
||||
business_management_token: business_management_token,
|
||||
validate_provider_config: false,
|
||||
sync_templates: false
|
||||
)
|
||||
end
|
||||
|
||||
let(:message) do
|
||||
create(:message, conversation: conversation, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox, source_id: 'external_id')
|
||||
@@ -282,21 +291,95 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
|
||||
describe '#sync_templates' do
|
||||
context 'when called' do
|
||||
context 'with a business management token' do
|
||||
let(:business_management_token) { 'business-token' }
|
||||
|
||||
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) }
|
||||
|
||||
it 'uses it instead of the provider API key' do
|
||||
request = stub_request(
|
||||
:get,
|
||||
'https://graph.facebook.com/v14.0/123456789/message_templates'
|
||||
).with(
|
||||
headers: { 'Authorization' => 'Bearer business-token' }
|
||||
).to_return(status: 200, headers: response_headers, body: { data: [] }.to_json)
|
||||
|
||||
subject.sync_templates
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a business management token' do
|
||||
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) }
|
||||
|
||||
it 'uses the provider API key' do
|
||||
request = stub_request(
|
||||
:get,
|
||||
'https://graph.facebook.com/v14.0/123456789/message_templates'
|
||||
).with(
|
||||
headers: { 'Authorization' => 'Bearer test_key' }
|
||||
).to_return(status: 200, headers: response_headers, body: { data: [] }.to_json)
|
||||
|
||||
subject.sync_templates
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a stored business management token outside Chatwoot Cloud' do
|
||||
let(:business_management_token) { 'business-token' }
|
||||
|
||||
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false) }
|
||||
|
||||
it 'uses the provider API key' do
|
||||
request = stub_request(
|
||||
:get,
|
||||
'https://graph.facebook.com/v14.0/123456789/message_templates'
|
||||
).with(
|
||||
headers: { 'Authorization' => 'Bearer test_key' }
|
||||
).to_return(status: 200, headers: response_headers, body: { data: [] }.to_json)
|
||||
|
||||
subject.sync_templates
|
||||
|
||||
expect(request).to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
it 'updated the message templates' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
|
||||
request_headers = { 'Authorization' => 'Bearer test_key' }
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates')
|
||||
.with(headers: request_headers)
|
||||
.to_return(
|
||||
{ status: 200, headers: response_headers,
|
||||
body: { data: [
|
||||
{ id: '123456789', name: 'test_template' }
|
||||
], paging: { next: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json },
|
||||
{ status: 200, headers: response_headers,
|
||||
body: { data: [
|
||||
{ id: '123456789', name: 'next_template' }
|
||||
], paging: { next: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json },
|
||||
{ status: 200, headers: response_headers,
|
||||
body: { data: [
|
||||
{ id: '123456789', name: 'last_template' }
|
||||
], paging: { prev: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json }
|
||||
status: 200,
|
||||
headers: response_headers,
|
||||
body: {
|
||||
data: [{ id: '123456789', name: 'test_template' }],
|
||||
paging: {
|
||||
cursors: { after: 'cursor-1' },
|
||||
next: 'https://graph.facebook.com/v14.0/123456789/message_templates?after=cursor-1&access_token=test_key'
|
||||
}
|
||||
}.to_json
|
||||
)
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?after=cursor-1')
|
||||
.with(headers: request_headers)
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: response_headers,
|
||||
body: {
|
||||
data: [{ id: '123456789', name: 'next_template' }],
|
||||
paging: {
|
||||
cursors: { after: 'cursor-2' },
|
||||
next: 'https://graph.facebook.com/v14.0/123456789/message_templates?after=cursor-2&access_token=test_key'
|
||||
}
|
||||
}.to_json
|
||||
)
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?after=cursor-2')
|
||||
.with(headers: request_headers)
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: response_headers,
|
||||
body: { data: [{ id: '123456789', name: 'last_template' }] }.to_json
|
||||
)
|
||||
|
||||
timstamp = whatsapp_channel.reload.message_templates_last_updated
|
||||
@@ -308,7 +391,8 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
end
|
||||
|
||||
it 'updates message_templates_last_updated even when template request fails' do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates')
|
||||
.with(headers: { 'Authorization' => 'Bearer test_key' })
|
||||
.to_return(status: 401)
|
||||
|
||||
timstamp = whatsapp_channel.reload.message_templates_last_updated
|
||||
|
||||
52
spec/services/whatsapp/reauthorization_service_spec.rb
Normal file
52
spec/services/whatsapp/reauthorization_service_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Whatsapp::ReauthorizationService do
|
||||
let(:account) { create(:account) }
|
||||
let(:channel) do
|
||||
create(
|
||||
:channel_whatsapp,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'api_key' => 'old-token',
|
||||
'phone_number_id' => 'old-phone-id',
|
||||
'business_account_id' => 'old-waba-id',
|
||||
'source' => 'embedded_signup'
|
||||
},
|
||||
business_management_token: 'business-token',
|
||||
validate_provider_config: false,
|
||||
sync_templates: false
|
||||
)
|
||||
end
|
||||
let(:inbox) { create(:inbox, account: account, channel: channel) }
|
||||
let(:phone_info) { { phone_number: channel.phone_number, business_name: inbox.name } }
|
||||
|
||||
before do
|
||||
stub_request(:get, %r{\Ahttps://graph\.facebook\.com/v14\.0/.+/message_templates\?access_token=new-token\z})
|
||||
.to_return(status: 200, body: { data: [] }.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
stub_request(:get, %r{\Ahttps://graph\.facebook\.com/v14\.0/.+/phone_numbers\?.*access_token=new-token})
|
||||
.to_return(status: 200, body: { data: [{ id: 'new-phone-id' }] }.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
end
|
||||
|
||||
it 'clears the business management token when the WhatsApp Business Account changes' do
|
||||
described_class.new(
|
||||
account: account,
|
||||
inbox_id: inbox.id,
|
||||
phone_number_id: 'new-phone-id',
|
||||
waba_id: 'new-waba-id'
|
||||
).perform('new-token', phone_info)
|
||||
|
||||
expect(channel.reload.business_management_token).to be_nil
|
||||
end
|
||||
|
||||
it 'retains the business management token when the WhatsApp Business Account does not change' do
|
||||
described_class.new(
|
||||
account: account,
|
||||
inbox_id: inbox.id,
|
||||
phone_number_id: 'new-phone-id',
|
||||
waba_id: channel.provider_config['business_account_id']
|
||||
).perform('new-token', phone_info)
|
||||
|
||||
expect(channel.reload.business_management_token).to eq('business-token')
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user