Merge branch 'develop' into feat/github-integration

This commit is contained in:
Muhsin Keloth
2025-07-29 12:50:05 +04:00
committed by GitHub
179 changed files with 1825 additions and 230 deletions
+7 -1
View File
@@ -36,12 +36,18 @@ describe ContactIdentifyAction do
expect(result.additional_attributes['social_profiles']).to eq({ 'linkedin' => 'saras', 'twitter' => 'saras' })
end
it 'enques avatar job when avatar url parameter is passed' do
it 'enqueues avatar job when valid avatar url parameter is passed' do
params = { name: 'test', avatar_url: 'https://chatwoot-assets.local/sample.png' }
expect(Avatar::AvatarFromUrlJob).to receive(:perform_later).with(contact, params[:avatar_url]).once
described_class.new(contact: contact, params: params).perform
end
it 'does not enqueue avatar job when invalid avatar url parameter is passed' do
params = { name: 'test', avatar_url: 'invalid-url' }
expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later)
described_class.new(contact: contact, params: params).perform
end
context 'when contact with same identifier exists' do
it 'merges the current contact to identified contact' do
existing_identified_contact = create(:contact, account: account, identifier: 'test_id')
@@ -904,4 +904,80 @@ RSpec.describe 'Inboxes API', type: :request do
end
end
end
describe 'POST /api/v1/accounts/{account.id}/inboxes/:id/sync_templates' do
let(:whatsapp_channel) do
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
end
let(:whatsapp_inbox) { create(:inbox, account: account, channel: whatsapp_channel) }
let(:non_whatsapp_inbox) { create(:inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/sync_templates"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated agent' do
it 'returns unauthorized for agent' do
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/sync_templates",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated administrator' do
context 'with WhatsApp inbox' do
it 'successfully initiates template sync' do
expect(Channels::Whatsapp::TemplatesSyncJob).to receive(:perform_later).with(whatsapp_channel)
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/sync_templates",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['message']).to eq('Template sync initiated successfully')
end
it 'handles job errors gracefully' do
allow(Channels::Whatsapp::TemplatesSyncJob).to receive(:perform_later).and_raise(StandardError, 'Job failed')
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/sync_templates",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:internal_server_error)
json_response = response.parsed_body
expect(json_response['error']).to eq('Job failed')
end
end
context 'with non-WhatsApp inbox' do
it 'returns unprocessable entity error' do
post "/api/v1/accounts/#{account.id}/inboxes/#{non_whatsapp_inbox.id}/sync_templates",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['error']).to eq('Template sync is only available for WhatsApp channels')
end
end
context 'with non-existent inbox' do
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/inboxes/999999/sync_templates",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end
end
@@ -33,5 +33,21 @@ RSpec.describe 'Webhooks::InstagramController', type: :request do
post '/webhooks/instagram', params: instagram_params
expect(response).to have_http_status(:success)
end
context 'when processing echo events' do
let!(:echo_params) { build(:instagram_story_mention_event_with_echo).with_indifferent_access }
it 'delays processing for echo events by 2 seconds' do
job_double = class_double(Webhooks::InstagramEventsJob)
allow(Webhooks::InstagramEventsJob).to receive(:set).with(wait: 2.seconds).and_return(job_double)
allow(job_double).to receive(:perform_later)
instagram_params = echo_params.merge(object: 'instagram')
post '/webhooks/instagram', params: instagram_params
expect(response).to have_http_status(:success)
expect(Webhooks::InstagramEventsJob).to have_received(:set).with(wait: 2.seconds)
expect(job_double).to have_received(:perform_later)
end
end
end
end
@@ -0,0 +1,47 @@
require 'rails_helper'
RSpec.describe Enterprise::CloudflareVerificationJob do
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
describe '#perform' do
context 'when portal is not found' do
it 'returns early' do
expect(Portal).to receive(:find).with(0).and_return(nil)
expect(Cloudflare::CheckCustomHostnameService).not_to receive(:new)
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
described_class.perform_now(0)
end
end
context 'when portal has no custom domain' do
it 'returns early' do
portal_without_domain = create(:portal, custom_domain: nil)
expect(Cloudflare::CheckCustomHostnameService).not_to receive(:new)
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
described_class.perform_now(portal_without_domain.id)
end
end
context 'when portal exists with custom domain' do
it 'checks hostname status' do
check_service = instance_double(Cloudflare::CheckCustomHostnameService, perform: { data: 'success' })
expect(Cloudflare::CheckCustomHostnameService).to receive(:new).with(portal: portal).and_return(check_service)
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
described_class.perform_now(portal.id)
end
it 'creates hostname when check returns errors' do
check_service = instance_double(Cloudflare::CheckCustomHostnameService, perform: { errors: ['Hostname is missing'] })
create_service = instance_double(Cloudflare::CreateCustomHostnameService, perform: { data: 'success' })
expect(Cloudflare::CheckCustomHostnameService).to receive(:new).with(portal: portal).and_return(check_service)
expect(Cloudflare::CreateCustomHostnameService).to receive(:new).with(portal: portal).and_return(create_service)
described_class.perform_now(portal.id)
end
end
end
end
@@ -0,0 +1,59 @@
require 'rails_helper'
RSpec.describe Enterprise::Concerns::Portal do
describe '#enqueue_cloudflare_verification' do
let(:portal) { create(:portal, custom_domain: nil) }
context 'when custom_domain is changed' do
context 'when on chatwoot cloud' do
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
end
it 'enqueues cloudflare verification job' do
expect do
portal.update(custom_domain: 'test.example.com')
end.to have_enqueued_job(Enterprise::CloudflareVerificationJob).with(portal.id)
end
end
context 'when not on chatwoot cloud' do
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
end
it 'does not enqueue cloudflare verification job' do
expect do
portal.update(custom_domain: 'test.example.com')
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
end
end
end
context 'when custom_domain is not changed' do
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
portal.update(custom_domain: 'test.example.com')
end
it 'does not enqueue cloudflare verification job' do
expect do
portal.update(name: 'New Name')
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
end
end
context 'when custom_domain is set to blank' do
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
portal.update(custom_domain: 'test.example.com')
end
it 'does not enqueue cloudflare verification job' do
expect do
portal.update(custom_domain: '')
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
end
end
end
end
@@ -22,6 +22,7 @@ RSpec.describe Captain::Copilot::ChatService do
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
create(:installation_config, name: 'CAPTAIN_OPEN_AI_ENDPOINT', value: 'https://api.openai.com/')
allow(OpenAI::Client).to receive(:new).and_return(mock_openai_client)
allow(mock_openai_client).to receive(:chat).and_return({
choices: [{ message: { content: '{ "content": "Hey" }' } }]
@@ -47,6 +48,48 @@ RSpec.describe Captain::Copilot::ChatService do
expect(messages.second[:role]).to eq('system')
expect(messages.second[:content]).to include(account.id.to_s)
end
it 'initializes OpenAI client with configured endpoint' do
expect(OpenAI::Client).to receive(:new).with(
access_token: 'test-key',
uri_base: 'https://api.openai.com/',
log_errors: Rails.env.development?
)
described_class.new(assistant, config)
end
context 'when CAPTAIN_OPEN_AI_ENDPOINT is not configured' do
before do
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy
end
it 'uses default OpenAI endpoint' do
expect(OpenAI::Client).to receive(:new).with(
access_token: 'test-key',
uri_base: 'https://api.openai.com/',
log_errors: Rails.env.development?
)
described_class.new(assistant, config)
end
end
context 'when custom endpoint is configured' do
before do
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT').update!(value: 'https://custom.azure.com/')
end
it 'uses custom endpoint for OpenAI client' do
expect(OpenAI::Client).to receive(:new).with(
access_token: 'test-key',
uri_base: 'https://custom.azure.com/',
log_errors: Rails.env.development?
)
described_class.new(assistant, config)
end
end
end
describe '#generate_response' do
@@ -0,0 +1,111 @@
require 'rails_helper'
RSpec.describe Cloudflare::CheckCustomHostnameService do
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
let(:installation_config_api_key) { create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-key') }
let(:installation_config_zone_id) { create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id') }
describe '#perform' do
context 'when API token or zone ID is not found' do
it 'returns error when API token is missing' do
installation_config_zone_id
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
end
it 'returns error when zone ID is missing' do
installation_config_api_key
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
end
end
context 'when no hostname ID is found' do
it 'returns error' do
installation_config_api_key
installation_config_zone_id
portal.update(custom_domain: nil)
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['No custom domain found'])
end
end
context 'when API request is made' do
before do
installation_config_api_key
installation_config_zone_id
end
context 'when API request fails' do
it 'returns error response' do
service = described_class.new(portal: portal)
error_response = {
'errors' => [{ 'message' => 'API error' }]
}
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
.to_return(status: 422, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:errors]).to eq(error_response['errors'])
end
end
context 'when API request succeeds but no data is returned' do
it 'returns hostname missing error' do
service = described_class.new(portal: portal)
success_response = {
'result' => []
}
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result).to eq(errors: ['Hostname is missing in Cloudflare'])
end
end
context 'when API request succeeds and data is returned' do
it 'updates portal SSL settings and returns success' do
service = described_class.new(portal: portal)
success_response = {
'result' => [
{
'ownership_verification_http' => {
'http_url' => 'http://example.com/.well-known/cf-verification/verification-id',
'http_body' => 'verification-body'
}
}
]
}
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
expect(portal).to receive(:update).with(
ssl_settings: {
'cf_verification_id': 'verification-id',
'cf_verification_body': 'verification-body'
}
)
result = service.perform
expect(result).to eq(data: success_response['result'])
end
end
end
end
end
@@ -0,0 +1,111 @@
require 'rails_helper'
RSpec.describe Cloudflare::CreateCustomHostnameService do
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
let(:installation_config_api_key) { create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-key') }
let(:installation_config_zone_id) { create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id') }
describe '#perform' do
context 'when API token or zone ID is not found' do
it 'returns error when API token is missing' do
installation_config_zone_id
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
end
it 'returns error when zone ID is missing' do
installation_config_api_key
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
end
end
context 'when no hostname is found' do
it 'returns error' do
installation_config_api_key
installation_config_zone_id
portal.update(custom_domain: nil)
service = described_class.new(portal: portal)
result = service.perform
expect(result).to eq(errors: ['No hostname found'])
end
end
context 'when API request is made' do
before do
installation_config_api_key
installation_config_zone_id
end
context 'when API request fails' do
it 'returns error response' do
service = described_class.new(portal: portal)
error_response = {
'errors' => [{ 'message' => 'API error' }]
}
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
body: { hostname: 'test.example.com' }.to_json)
.to_return(status: 422, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result[:errors]).to eq(error_response['errors'])
end
end
context 'when API request succeeds but no data is returned' do
it 'returns hostname creation error' do
service = described_class.new(portal: portal)
success_response = {
'result' => nil
}
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
body: { hostname: 'test.example.com' }.to_json)
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.perform
expect(result).to eq(errors: ['Could not create hostname'])
end
end
context 'when API request succeeds and data is returned' do
it 'updates portal SSL settings and returns success' do
service = described_class.new(portal: portal)
success_response = {
'result' => {
'ownership_verification_http' => {
'http_url' => 'http://example.com/.well-known/cf-verification/verification-id',
'http_body' => 'verification-body'
}
}
}
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
body: { hostname: 'test.example.com' }.to_json)
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
expect(portal).to receive(:update).with(ssl_settings: { 'cf_verification_id': 'verification-id',
'cf_verification_body': 'verification-body' })
result = service.perform
expect(result).to eq(data: success_response['result'])
end
end
end
end
end
@@ -7,7 +7,7 @@ RSpec.describe Avatar::AvatarFromGravatarJob do
it 'enqueues the job' do
expect { described_class.perform_later(avatarable, email) }.to have_enqueued_job(described_class)
.on_queue('low')
.on_queue('purgable')
end
it 'will call AvatarFromUrlJob with gravatar url' do
+1 -1
View File
@@ -6,7 +6,7 @@ RSpec.describe Avatar::AvatarFromUrlJob do
it 'enqueues the job' do
expect { described_class.perform_later(avatarable, avatar_url) }.to have_enqueued_job(described_class)
.on_queue('low')
.on_queue('purgable')
end
it 'will attach avatar from url' do
@@ -279,11 +279,29 @@ describe Webhooks::InstagramEventsJob do
expect(instagram_inbox.messages.count).to be 0
end
it 'handle messaging_seen callback' do
it 'handles messaging_seen callback' do
expect(Instagram::ReadStatusService).to receive(:new).with(params: message_events[:messaging_seen][:entry][0][:messaging][0],
channel: instagram_inbox.channel).and_call_original
instagram_webhook.perform_now(message_events[:messaging_seen][:entry])
end
it 'creates contact when Instagram API call returns `No matching Instagram user` (9010 error code)' do
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
.to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json)
instagram_webhook.perform_now(message_events[:dm][:entry])
instagram_inbox.reload
expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.contacts.last.name).to eq 'Unknown (IG: Sender-id-1)'
expect(instagram_inbox.contacts.last.contact_inboxes.count).to be 1
expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq 'Sender-id-1'
expect(instagram_inbox.conversations.count).to eq 1
expect(instagram_inbox.messages.count).to eq 1
expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be_nil
end
end
end
end
+56
View File
@@ -122,4 +122,60 @@ RSpec.describe Channel::Whatsapp do
end
end
end
describe '#teardown_webhooks' do
let(:account) { create(:account) }
context 'when channel is whatsapp_cloud with embedded_signup' do
it 'calls WebhookTeardownService on destroy' do
# Mock the setup service to prevent HTTP calls during creation
setup_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(setup_service)
allow(setup_service).to receive(:perform)
channel = create(:channel_whatsapp,
account: account,
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token',
'phone_number_id' => '123456789'
},
validate_provider_config: false,
sync_templates: false)
teardown_service = instance_double(Whatsapp::WebhookTeardownService)
allow(Whatsapp::WebhookTeardownService).to receive(:new).with(channel).and_return(teardown_service)
allow(teardown_service).to receive(:perform)
channel.destroy
expect(Whatsapp::WebhookTeardownService).to have_received(:new).with(channel)
expect(teardown_service).to have_received(:perform)
end
end
context 'when channel is not embedded_signup' do
it 'does not call WebhookTeardownService on destroy' do
channel = create(:channel_whatsapp,
account: account,
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'manual',
'api_key' => 'test_access_token'
},
validate_provider_config: false,
sync_templates: false)
teardown_service = instance_double(Whatsapp::WebhookTeardownService)
allow(Whatsapp::WebhookTeardownService).to receive(:new).with(channel).and_return(teardown_service)
allow(teardown_service).to receive(:perform)
channel.destroy
expect(teardown_service).to have_received(:perform)
end
end
end
end
+94
View File
@@ -102,4 +102,98 @@ RSpec.describe Contact do
expect(contact.contact_type).to eq 'lead'
end
end
describe '.resolved_contacts' do
let(:account) { create(:account) }
context 'when crm_v2 feature flag is disabled' do
it 'returns contacts with email, phone_number, or identifier using feature flag value' do
# Create contacts with different attributes
contact_with_email = create(:contact, account: account, email: 'test@example.com', name: 'John Doe')
contact_with_phone = create(:contact, account: account, phone_number: '+1234567890', name: 'Jane Smith')
contact_with_identifier = create(:contact, account: account, identifier: 'user123', name: 'Bob Wilson')
contact_without_details = create(:contact, account: account, name: 'Alice Johnson', email: nil, phone_number: nil, identifier: nil)
resolved = account.contacts.resolved_contacts(use_crm_v2: false)
expect(resolved).to include(contact_with_email, contact_with_phone, contact_with_identifier)
expect(resolved).not_to include(contact_without_details)
end
end
context 'when crm_v2 feature flag is enabled' do
it 'returns only contacts with contact_type lead' do
# Contact with email and phone - should be marked as lead
contact_with_details = create(:contact, account: account, email: 'customer@example.com', phone_number: '+1234567890', name: 'Customer One')
expect(contact_with_details.contact_type).to eq('lead')
# Contact without email/phone - should be marked as visitor
contact_without_details = create(:contact, account: account, name: 'Lead', email: nil, phone_number: nil)
expect(contact_without_details.contact_type).to eq('visitor')
# Force set contact_type to lead for testing
contact_without_details.update!(contact_type: 'lead')
resolved = account.contacts.resolved_contacts(use_crm_v2: true)
expect(resolved).to include(contact_with_details)
expect(resolved).to include(contact_without_details)
end
it 'includes all lead contacts regardless of email/phone presence' do
# Create a lead contact with only name
lead_contact = create(:contact, account: account, name: 'Test Lead')
lead_contact.update!(contact_type: 'lead')
# Create a customer contact
customer_contact = create(:contact, account: account, email: 'customer@test.com')
customer_contact.update!(contact_type: 'customer')
# Create a visitor contact
visitor_contact = create(:contact, account: account, name: 'Visitor')
expect(visitor_contact.contact_type).to eq('visitor')
resolved = account.contacts.resolved_contacts(use_crm_v2: true)
expect(resolved).to include(lead_contact)
expect(resolved).not_to include(customer_contact)
expect(resolved).not_to include(visitor_contact)
end
it 'returns contacts with email, phone_number, or identifier when explicitly passing use_crm_v2: false' do
# Even though feature flag is enabled, we're explicitly passing false
contact_with_email = create(:contact, account: account, email: 'test@example.com', name: 'John Doe')
contact_with_phone = create(:contact, account: account, phone_number: '+1234567890', name: 'Jane Smith')
contact_without_details = create(:contact, account: account, name: 'Alice Johnson', email: nil, phone_number: nil, identifier: nil)
resolved = account.contacts.resolved_contacts(use_crm_v2: false)
# Should use the old logic despite feature flag being enabled
expect(resolved).to include(contact_with_email, contact_with_phone)
expect(resolved).not_to include(contact_without_details)
end
end
context 'with mixed contact types' do
it 'correctly filters based on use_crm_v2 parameter regardless of feature flag' do
# Create different types of contacts
visitor_contact = create(:contact, account: account, name: 'Visitor')
lead_with_email = create(:contact, account: account, email: 'lead@example.com', name: 'Lead')
lead_without_email = create(:contact, account: account, name: 'Lead Only')
lead_without_email.update!(contact_type: 'lead')
customer_contact = create(:contact, account: account, email: 'customer@example.com', name: 'Customer')
customer_contact.update!(contact_type: 'customer')
# Test with use_crm_v2: false
resolved_old = account.contacts.resolved_contacts(use_crm_v2: false)
expect(resolved_old).to include(lead_with_email, customer_contact)
expect(resolved_old).not_to include(visitor_contact, lead_without_email)
# Test with use_crm_v2: true
resolved_new = account.contacts.resolved_contacts(use_crm_v2: true)
expect(resolved_new).to include(lead_with_email, lead_without_email)
expect(resolved_new).not_to include(visitor_contact, customer_contact)
end
end
end
end
+57
View File
@@ -185,6 +185,63 @@ describe SearchService do
end
end
describe '#message_base_query' do
let(:params) { { q: 'test' } }
let(:search_type) { 'Message' }
context 'when user is admin' do
let(:admin_user) { create(:user) }
let(:admin_search) do
create(:account_user, account: account, user: admin_user, role: 'administrator')
described_class.new(current_user: admin_user, current_account: account, params: params, search_type: search_type)
end
it 'does not filter by inbox_id' do
# Testing the private method itself seems like the best way to ensure
# that the inboxes are not added to the search query
base_query = admin_search.send(:message_base_query)
# Should only have the time filter, not inbox filter
expect(base_query.to_sql).to include('created_at >= ')
expect(base_query.to_sql).not_to include('inbox_id')
end
end
context 'when user is not admin' do
before do
account_user = account.account_users.find_or_create_by(user: user)
account_user.update!(role: 'agent')
end
it 'filters by accessible inbox_id when user has limited access' do
# Create an additional inbox that user is NOT assigned to
create(:inbox, account: account)
base_query = search.send(:message_base_query)
# Should have both time and inbox filters
expect(base_query.to_sql).to include('created_at >= ')
expect(base_query.to_sql).to include('inbox_id')
end
context 'when user has access to all inboxes' do
before do
# Create additional inbox and assign user to all inboxes
other_inbox = create(:inbox, account: account)
create(:inbox_member, user: user, inbox: other_inbox)
end
it 'skips inbox filtering as optimization' do
base_query = search.send(:message_base_query)
# Should only have the time filter, not inbox filter
expect(base_query.to_sql).to include('created_at >= ')
expect(base_query.to_sql).not_to include('inbox_id')
end
end
end
end
describe '#use_gin_search' do
let(:params) { { q: 'test' } }
@@ -194,4 +194,41 @@ describe Whatsapp::FacebookApiClient do
end
end
end
describe '#unsubscribe_waba_webhook' do
let(:waba_id) { 'test_waba_id' }
context 'when successful' do
before do
stub_request(:delete, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
)
.to_return(
status: 200,
body: { success: true }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns success response' do
result = api_client.unsubscribe_waba_webhook(waba_id)
expect(result['success']).to be(true)
end
end
context 'when failed' do
before do
stub_request(:delete, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' }
)
.to_return(status: 400, body: { error: 'Webhook unsubscription failed' }.to_json)
end
it 'raises an error' do
expect { api_client.unsubscribe_waba_webhook(waba_id) }.to raise_error(/Webhook unsubscription failed/)
end
end
end
end
@@ -0,0 +1,81 @@
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
channel.update!(
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'business_account_id' => 'test_waba_id',
'api_key' => 'test_api_key'
}
)
end
it 'calls unsubscribe_waba_webhook 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(:unsubscribe_waba_webhook).with('test_waba_id')
service.perform
expect(api_client).to have_received(:unsubscribe_waba_webhook).with('test_waba_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(:unsubscribe_waba_webhook).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 but not embedded_signup' do
before do
channel.update!(
provider: 'whatsapp_cloud',
provider_config: { 'source' => 'manual' }
)
end
it 'does not attempt to unsubscribe webhook' do
expect(Whatsapp::FacebookApiClient).not_to receive(:new)
service.perform
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