From 802f0694ed9971d5ab441bd06b82638e403cc1bb Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:46:15 +0530 Subject: [PATCH 1/3] chore: Alphabetically sort inbox list on settings page (#11921) # Pull Request Template ## Description This PR updates the inbox list on the settings page to be sorted alphabetically. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshot image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../routes/dashboard/settings/inbox/Index.vue | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue index aae2263a7..c4cf5f5ad 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue @@ -1,12 +1,16 @@ diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb index e72880944..0adf073cd 100644 --- a/app/services/whatsapp/embedded_signup_service.rb +++ b/app/services/whatsapp/embedded_signup_service.rb @@ -10,8 +10,6 @@ class Whatsapp::EmbeddedSignupService def perform validate_parameters! - GlobalConfig.clear_cache - # Exchange code for user access token access_token = Whatsapp::TokenExchangeService.new(@code).perform diff --git a/config/features.yml b/config/features.yml index 5171b1c01..36a84f4a6 100644 --- a/config/features.yml +++ b/config/features.yml @@ -176,3 +176,6 @@ - name: notion_integration display_name: Notion Integration enabled: false +- name: whatsapp_embedded_signup + display_name: WhatsApp Embedded Signup + enabled: false diff --git a/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb new file mode 100644 index 000000000..ab279a131 --- /dev/null +++ b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb @@ -0,0 +1,279 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Accounts::Whatsapp::AuthorizationsController, type: :controller do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + + describe 'POST #create' do + context 'when user is not authenticated' do + it 'returns unauthorized' do + post :create, params: { account_id: account.id } + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when user is authenticated' do + before { sign_in(agent) } + + context 'when feature is not enabled' do + before do + account.disable_features!(:whatsapp_embedded_signup) + end + + it 'returns forbidden' do + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body['error']).to eq('WhatsApp embedded signup is not enabled for this account') + end + end + + context 'when feature is enabled' do + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'returns unprocessable entity when code is missing' do + post :create, params: { + account_id: account.id, + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('code') + end + + it 'returns unprocessable entity when business_id is missing' do + post :create, params: { + account_id: account.id, + code: 'test_code', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('business_id') + end + + it 'returns unprocessable entity when waba_id is missing' do + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('waba_id') + end + + it 'creates whatsapp channel successfully' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service) + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service to prevent HTTP calls + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + } + + expect(response).to have_http_status(:success) + response_data = response.parsed_body + expect(response_data['success']).to be true + expect(response_data['id']).to eq(inbox.id) + expect(response_data['name']).to eq(inbox.name) + expect(response_data['channel_type']).to eq('whatsapp') + end + + it 'calls the embedded signup service with correct parameters' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + expect(Whatsapp::EmbeddedSignupService).to receive(:new).with( + account: account, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + ).and_return(embedded_signup_service) + + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + } + end + + it 'accepts phone_number_id as optional parameter' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + expect(Whatsapp::EmbeddedSignupService).to receive(:new).with( + account: account, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: nil + ).and_return(embedded_signup_service) + + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:success) + end + + it 'returns unprocessable entity when service fails' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error') + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + response_data = response.parsed_body + expect(response_data['success']).to be false + expect(response_data['error']).to eq('Service error') + end + + it 'logs error when service fails' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error') + + expect(Rails.logger).to receive(:error).with(/\[WHATSAPP AUTHORIZATION\] Embedded signup error: Service error/) + expect(Rails.logger).to receive(:error).with(/authorizations_controller/) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + end + + it 'handles token exchange errors' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new) + .and_raise(StandardError, 'Invalid authorization code') + + post :create, params: { + account_id: account.id, + code: 'invalid_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('Invalid authorization code') + end + + it 'handles channel already exists error' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new) + .and_raise(StandardError, 'Channel already exists') + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('Channel already exists') + end + end + + context 'when user is not authorized for the account' do + let(:other_account) { create(:account) } + + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'returns unauthorized' do + post :create, params: { + account_id: other_account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when user is an administrator' do + let(:administrator) { create(:user, account: account, role: :administrator) } + + before do + sign_in(administrator) + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'allows channel creation' do + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service) + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:success) + end + end + end + end +end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 4368807fe..34ef264b5 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -61,4 +61,85 @@ RSpec.describe Channel::Whatsapp do expect(channel.provider_config['webhook_verify_token']).to eq '123' end end + + describe 'webhook setup after creation' do + let(:account) { create(:account) } + let(:webhook_service) { instance_double(Whatsapp::WebhookSetupService) } + + before do + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + end + + context 'when channel is created through embedded signup' do + it 'sets up webhooks automatically' do + expect(Whatsapp::WebhookSetupService).to receive(:new).with( + anything, + 'test_waba_id', + 'test_access_token' + ) + expect(webhook_service).to receive(:perform) + + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'source' => 'embedded_signup', + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end + + it 'does not raise error if webhook setup fails' do + allow(webhook_service).to receive(:perform).and_raise(StandardError, 'Webhook error') + + expect do + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'source' => 'embedded_signup', + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end.not_to raise_error + end + end + + context 'when channel is created through manual setup' do + it 'does not setup webhooks' do + expect(Whatsapp::WebhookSetupService).not_to receive(:new) + + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end + end + + context 'when channel is created with different provider' do + it 'does not setup webhooks for 360dialog provider' do + expect(Whatsapp::WebhookSetupService).not_to receive(:new) + + create(:channel_whatsapp, + account: account, + provider: 'default', + provider_config: { + 'source' => 'embedded_signup', + 'api_key' => 'test_360dialog_key' + }, + validate_provider_config: false, + sync_templates: false) + end + end + end end diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb new file mode 100644 index 000000000..1cf0b87f2 --- /dev/null +++ b/spec/models/concerns/featurable_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Featurable do + let(:account) { create(:account) } + + describe 'WhatsApp embedded signup feature' do + it 'is disabled by default' do + expect(account.feature_whatsapp_embedded_signup?).to be false + expect(account.feature_enabled?('whatsapp_embedded_signup')).to be false + end + + describe '#enable_features!' do + it 'enables the whatsapp embedded signup feature' do + account.enable_features!(:whatsapp_embedded_signup) + expect(account.feature_whatsapp_embedded_signup?).to be true + expect(account.feature_enabled?('whatsapp_embedded_signup')).to be true + end + + it 'enables multiple features at once' do + account.enable_features!(:whatsapp_embedded_signup, :help_center) + expect(account.feature_whatsapp_embedded_signup?).to be true + expect(account.feature_help_center?).to be true + end + end + + describe '#disable_features!' do + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'disables the whatsapp embedded signup feature' do + expect(account.feature_whatsapp_embedded_signup?).to be true + + account.disable_features!(:whatsapp_embedded_signup) + expect(account.feature_whatsapp_embedded_signup?).to be false + end + end + + describe '#enabled_features' do + it 'includes whatsapp_embedded_signup when enabled' do + account.enable_features!(:whatsapp_embedded_signup) + expect(account.enabled_features).to include('whatsapp_embedded_signup' => true) + end + + it 'does not include whatsapp_embedded_signup when disabled' do + account.disable_features!(:whatsapp_embedded_signup) + expect(account.enabled_features).not_to include('whatsapp_embedded_signup' => true) + end + end + + describe '#all_features' do + it 'includes whatsapp_embedded_signup in all features list' do + expect(account.all_features).to have_key('whatsapp_embedded_signup') + end + end + end +end diff --git a/spec/services/whatsapp/channel_creation_service_spec.rb b/spec/services/whatsapp/channel_creation_service_spec.rb index 80d1e378b..1c1f46232 100644 --- a/spec/services/whatsapp/channel_creation_service_spec.rb +++ b/spec/services/whatsapp/channel_creation_service_spec.rb @@ -19,6 +19,11 @@ describe Whatsapp::ChannelCreationService do # Clean up any existing channels to avoid phone number conflicts Channel::Whatsapp.destroy_all + # Stub the webhook setup service to prevent HTTP calls during tests + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + # Stub the provider validation and sync_templates allow(Channel::Whatsapp).to receive(:new).and_wrap_original do |method, *args| channel = method.call(*args) @@ -111,4 +116,4 @@ describe Whatsapp::ChannelCreationService do end end end -end \ No newline at end of file +end diff --git a/spec/services/whatsapp/embedded_signup_service_spec.rb b/spec/services/whatsapp/embedded_signup_service_spec.rb index 3c420f497..c799f8c83 100644 --- a/spec/services/whatsapp/embedded_signup_service_spec.rb +++ b/spec/services/whatsapp/embedded_signup_service_spec.rb @@ -32,7 +32,6 @@ describe Whatsapp::EmbeddedSignupService do let(:phone_info_service) { instance_double(Whatsapp::PhoneInfoService) } let(:token_validation_service) { instance_double(Whatsapp::TokenValidationService) } let(:channel_creation_service) { instance_double(Whatsapp::ChannelCreationService) } - let(:webhook_setup_service) { instance_double(Whatsapp::WebhookSetupService) } before do allow(GlobalConfig).to receive(:clear_cache) @@ -53,9 +52,9 @@ describe Whatsapp::EmbeddedSignupService do .and_return(channel_creation_service) allow(channel_creation_service).to receive(:perform).and_return(channel) - allow(Whatsapp::WebhookSetupService).to receive(:new) - .with(channel, waba_id, access_token).and_return(webhook_setup_service) - allow(webhook_setup_service).to receive(:perform) + # Webhook setup is now handled in the channel after_create callback + # So we stub it at the channel level + allow(channel).to receive(:setup_webhooks) end it 'orchestrates all services in the correct order' do @@ -64,7 +63,6 @@ describe Whatsapp::EmbeddedSignupService do expect(phone_info_service).to receive(:perform).ordered expect(token_validation_service).to receive(:perform).ordered expect(channel_creation_service).to receive(:perform).ordered - expect(webhook_setup_service).to receive(:perform).ordered result = service.perform expect(result).to eq(channel) @@ -125,4 +123,4 @@ describe Whatsapp::EmbeddedSignupService do end end end -end \ No newline at end of file +end