Merge branch 'develop' into feat/email-forwarding
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe V2::Reports::LabelSummaryBuilder do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let_it_be(:account) { create(:account) }
|
||||
let_it_be(:label_1) { create(:label, title: 'label_1', account: account) }
|
||||
let_it_be(:label_2) { create(:label, title: 'label_2', account: account) }
|
||||
let_it_be(:label_3) { create(:label, title: 'label_3', account: account) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
business_hours: business_hours,
|
||||
since: (Time.zone.today - 3.days).to_time.to_i.to_s,
|
||||
until: Time.zone.today.end_of_day.to_time.to_i.to_s,
|
||||
timezone_offset: 0
|
||||
}
|
||||
end
|
||||
let(:builder) { described_class.new(account: account, params: params) }
|
||||
|
||||
describe '#initialize' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'sets account and params' do
|
||||
expect(builder.account).to eq(account)
|
||||
expect(builder.params).to eq(params)
|
||||
end
|
||||
|
||||
it 'sets timezone from timezone_offset' do
|
||||
builder_with_offset = described_class.new(account: account, params: { timezone_offset: -8 })
|
||||
expect(builder_with_offset.instance_variable_get(:@timezone)).to eq('Pacific Time (US & Canada)')
|
||||
end
|
||||
|
||||
it 'defaults timezone when timezone_offset is not provided' do
|
||||
builder_without_offset = described_class.new(account: account, params: {})
|
||||
expect(builder_without_offset.instance_variable_get(:@timezone)).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build' do
|
||||
context 'when there are no labels' do
|
||||
let(:business_hours) { false }
|
||||
let(:empty_account) { create(:account) }
|
||||
let(:empty_builder) { described_class.new(account: empty_account, params: params) }
|
||||
|
||||
it 'returns empty array' do
|
||||
expect(empty_builder.build).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are labels but no conversations' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns zero values for all labels' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
bug_report = report.find { |r| r[:name] == 'label_1' }
|
||||
feature_request = report.find { |r| r[:name] == 'label_2' }
|
||||
customer_support = report.find { |r| r[:name] == 'label_3' }
|
||||
|
||||
[
|
||||
[bug_report, label_1, 'label_1'],
|
||||
[feature_request, label_2, 'label_2'],
|
||||
[customer_support, label_3, 'label_3']
|
||||
].each do |report_data, label, label_name|
|
||||
expect(report_data).to include(
|
||||
id: label.id,
|
||||
name: label_name,
|
||||
conversations_count: 0,
|
||||
avg_resolution_time: 0,
|
||||
avg_first_response_time: 0,
|
||||
avg_reply_time: 0,
|
||||
resolved_conversations_count: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are labeled conversations with metrics' do
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
# Create conversations with label_1
|
||||
3.times do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
create_list(:message, 2, message_type: 'outgoing',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 1.hour)
|
||||
create_list(:message, 1, message_type: 'incoming',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 2.hours)
|
||||
conversation.update_labels('label_1')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
# Create conversations with label_2
|
||||
2.times do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
create_list(:message, 1, message_type: 'outgoing',
|
||||
account: account, inbox: inbox,
|
||||
conversation: conversation,
|
||||
created_at: Time.zone.today + 1.hour)
|
||||
conversation.update_labels('label_2')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
# Resolve some conversations
|
||||
conversations_to_resolve = account.conversations.first(2)
|
||||
conversations_to_resolve.each(&:toggle_status)
|
||||
|
||||
# Create some reporting events
|
||||
account.conversations.reload.each_with_index do |conv, idx|
|
||||
# First response times
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'first_response',
|
||||
value: (30 + (idx * 10)) * 60,
|
||||
value_in_business_hours: (20 + (idx * 5)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
|
||||
# Reply times
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'reply',
|
||||
value: (15 + (idx * 5)) * 60,
|
||||
value_in_business_hours: (10 + (idx * 3)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
|
||||
# Resolution times for resolved conversations
|
||||
next unless conv.resolved?
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'conversation_resolved',
|
||||
value: (60 + (idx * 30)) * 60,
|
||||
value_in_business_hours: (45 + (idx * 20)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is disabled' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
it 'returns correct label stats using regular values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
label_2_report = report.find { |r| r[:name] == 'label_2' }
|
||||
label_3_report = report.find { |r| r[:name] == 'label_3' }
|
||||
|
||||
expect(label_1_report).to include(
|
||||
conversations_count: 3,
|
||||
avg_first_response_time: be > 0,
|
||||
avg_reply_time: be > 0
|
||||
)
|
||||
|
||||
expect(label_2_report).to include(
|
||||
conversations_count: 2,
|
||||
avg_first_response_time: be > 0,
|
||||
avg_reply_time: be > 0
|
||||
)
|
||||
|
||||
expect(label_3_report).to include(
|
||||
conversations_count: 0,
|
||||
avg_first_response_time: 0,
|
||||
avg_reply_time: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours is enabled' do
|
||||
let(:business_hours) { true }
|
||||
|
||||
it 'returns correct label stats using business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
label_2_report = report.find { |r| r[:name] == 'label_2' }
|
||||
|
||||
expect(label_1_report[:conversations_count]).to eq(3)
|
||||
expect(label_1_report[:avg_first_response_time]).to be > 0
|
||||
expect(label_1_report[:avg_reply_time]).to be > 0
|
||||
|
||||
expect(label_2_report[:conversations_count]).to eq(2)
|
||||
expect(label_2_report[:avg_first_response_time]).to be > 0
|
||||
expect(label_2_report[:avg_reply_time]).to be > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by date range' do
|
||||
let(:business_hours) { false }
|
||||
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
# Conversation within range
|
||||
conversation_in_range = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: 2.days.ago)
|
||||
conversation_in_range.update_labels('label_1')
|
||||
conversation_in_range.label_list
|
||||
conversation_in_range.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation_in_range,
|
||||
name: 'first_response',
|
||||
value: 1800,
|
||||
created_at: 2.days.ago)
|
||||
|
||||
# Conversation outside range (too old)
|
||||
conversation_out_of_range = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: 1.week.ago)
|
||||
conversation_out_of_range.update_labels('label_1')
|
||||
conversation_out_of_range.label_list
|
||||
conversation_out_of_range.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation_out_of_range,
|
||||
name: 'first_response',
|
||||
value: 3600,
|
||||
created_at: 1.week.ago)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'only includes conversations within the date range' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
expect(label_1_report).not_to be_nil
|
||||
expect(label_1_report[:conversations_count]).to eq(1)
|
||||
expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with business hours parameter' do
|
||||
let(:business_hours) { 'true' }
|
||||
|
||||
before do
|
||||
travel_to(Time.zone.today) do
|
||||
user = create(:user, account: account)
|
||||
inbox = create(:inbox, account: account)
|
||||
create(:inbox_member, user: user, inbox: inbox)
|
||||
|
||||
gravatar_url = 'https://www.gravatar.com'
|
||||
stub_request(:get, /#{gravatar_url}.*/).to_return(status: 404)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
conversation = create(:conversation, account: account,
|
||||
inbox: inbox, assignee: user,
|
||||
created_at: Time.zone.today)
|
||||
conversation.update_labels('label_1')
|
||||
conversation.label_list
|
||||
conversation.save!
|
||||
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conversation,
|
||||
name: 'first_response',
|
||||
value: 3600,
|
||||
value_in_business_hours: 1800,
|
||||
created_at: Time.zone.today)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'properly casts string "true" to boolean and uses business hours values' do
|
||||
report = builder.build
|
||||
|
||||
expect(report.length).to eq(3)
|
||||
|
||||
label_1_report = report.find { |r| r[:name] == 'label_1' }
|
||||
expect(label_1_report).not_to be_nil
|
||||
expect(label_1_report[:avg_first_response_time]).to eq(1800.0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -926,4 +926,63 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:id' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated agent' do
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['error']).to eq('You are not authorized to do this action')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated administrator' do
|
||||
before do
|
||||
create(:inbox_member, user: administrator, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
it 'successfully deletes the conversation' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(DeleteObjectJob).with(conversation, administrator, anything)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'can delete conversations from inboxes without direct access' do
|
||||
other_inbox = create(:inbox, account: account)
|
||||
other_conversation = create(:conversation, account: account, inbox: other_inbox)
|
||||
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{other_conversation.display_id}",
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(DeleteObjectJob).with(other_conversation, administrator, anything)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ RSpec.describe 'Integration Hooks API', type: :request do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' } } } }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' }, region: 'europe-west1' } } }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
|
||||
@@ -94,6 +94,18 @@ RSpec.describe 'Profile API', type: :request do
|
||||
expect(agent.reload.valid_password?('Test1234!')).to be true
|
||||
end
|
||||
|
||||
it 'does not reset the display name if updates the password' do
|
||||
display_name = agent.display_name
|
||||
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test123!', password: 'Test1234!', password_confirmation: 'Test1234!' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(agent.reload.display_name).to eq(display_name)
|
||||
end
|
||||
|
||||
it 'throws error when current password provided is invalid' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test', password: 'test123', password_confirmation: 'test123' } },
|
||||
|
||||
@@ -19,9 +19,10 @@ describe '/app/login', type: :request do
|
||||
end
|
||||
|
||||
context 'with non-HTML format' do
|
||||
it 'returns not acceptable for JSON' do
|
||||
get '/app/login', params: { format: 'json' }
|
||||
it 'returns not acceptable for JSON with error message' do
|
||||
get '/app/login', headers: { 'Accept' => 'application/json' }
|
||||
expect(response).to have_http_status(:not_acceptable)
|
||||
expect(response.parsed_body).to eq({ 'error' => 'Please use API routes instead of dashboard routes for JSON requests' })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Messages::AudioTranscriptionJob do
|
||||
subject(:job) { described_class.perform_later(attachment_id) }
|
||||
|
||||
let(:message) { create(:message) }
|
||||
let(:attachment) do
|
||||
message.attachments.create!(
|
||||
account_id: message.account_id,
|
||||
file_type: :audio,
|
||||
file: fixture_file_upload('public/audio/widget/ding.mp3')
|
||||
)
|
||||
end
|
||||
let(:attachment_id) { attachment.id }
|
||||
let(:conversation) { message.conversation }
|
||||
let(:transcription_service) { instance_double(Messages::AudioTranscriptionService) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(attachment_id)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when performing the job' do
|
||||
before do
|
||||
allow(Messages::AudioTranscriptionService).to receive(:new).with(attachment).and_return(transcription_service)
|
||||
allow(transcription_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'calls AudioTranscriptionService with the attachment' do
|
||||
expect(Messages::AudioTranscriptionService).to receive(:new).with(attachment)
|
||||
expect(transcription_service).to receive(:perform)
|
||||
described_class.perform_now(attachment_id)
|
||||
end
|
||||
|
||||
it 'does nothing when attachment is not found' do
|
||||
expect(Messages::AudioTranscriptionService).not_to receive(:new)
|
||||
described_class.perform_now(999_999)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Messages::AudioTranscriptionService, type: :service do
|
||||
let(:account) { create(:account, audio_transcriptions: true) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:message) { create(:message, conversation: conversation) }
|
||||
let(:attachment) { message.attachments.create!(account: account, file_type: :audio) }
|
||||
|
||||
before do
|
||||
# Create required installation configs
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-api-key')
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4o-mini')
|
||||
|
||||
# Mock usage limits for transcription to be available
|
||||
allow(account).to receive(:usage_limits).and_return({ captain: { responses: { current_available: 100 } } })
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:service) { described_class.new(attachment) }
|
||||
|
||||
context 'when transcription is successful' do
|
||||
before do
|
||||
# Mock can_transcribe? to return true and transcribe_audio method
|
||||
allow(service).to receive(:can_transcribe?).and_return(true)
|
||||
allow(service).to receive(:transcribe_audio).and_return('Hello world transcription')
|
||||
end
|
||||
|
||||
it 'returns successful transcription' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ success: true, transcriptions: 'Hello world transcription' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when audio transcriptions are disabled' do
|
||||
before do
|
||||
account.update!(audio_transcriptions: false)
|
||||
end
|
||||
|
||||
it 'returns error for transcription limit exceeded' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ error: 'Transcription limit exceeded' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attachment already has transcribed text' do
|
||||
before do
|
||||
attachment.update!(meta: { transcribed_text: 'Existing transcription' })
|
||||
allow(service).to receive(:can_transcribe?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns existing transcription without calling API' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ success: true, transcriptions: 'Existing transcription' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ FactoryBot.define do
|
||||
|
||||
trait :dialogflow do
|
||||
app_id { 'dialogflow' }
|
||||
settings { { project_id: 'test', credentials: {} } }
|
||||
settings { { project_id: 'test', credentials: {}, region: 'global' } }
|
||||
end
|
||||
|
||||
trait :dyte do
|
||||
|
||||
@@ -234,16 +234,15 @@ describe Webhooks::InstagramEventsJob do
|
||||
account_id: instagram_inbox.account_id
|
||||
)
|
||||
|
||||
instagram_inbox.reload
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
|
||||
instagram_webhook.perform_now(message_events[:unsend][:entry])
|
||||
|
||||
expect(instagram_inbox.messages.last.content).to eq 'This message was deleted'
|
||||
expect(instagram_inbox.messages.last.deleted).to be true
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 0
|
||||
expect(instagram_inbox.messages.last.reload.deleted).to be true
|
||||
message.reload
|
||||
|
||||
expect(message.content).to eq 'This message was deleted'
|
||||
expect(message.deleted).to be true
|
||||
expect(message.attachments.count).to be 0
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram direct inbox' do
|
||||
|
||||
@@ -175,4 +175,65 @@ describe Integrations::Dialogflow::ProcessorService do
|
||||
.to change(hook, :status).from('enabled').to('disabled')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'region configuration' do
|
||||
let(:processor) { described_class.new(event_name: event_name, hook: hook, event_data: event_data) }
|
||||
|
||||
context 'when region is global or not specified' do
|
||||
it 'uses global endpoint and session path' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {} })
|
||||
|
||||
expect(processor.send(:dialogflow_endpoint)).to eq('dialogflow.googleapis.com')
|
||||
expect(processor.send(:build_session_path, 'test-session')).to eq('projects/test-project/agent/sessions/test-session')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when region is specified' do
|
||||
it 'uses regional endpoint and session path' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
|
||||
expect(processor.send(:dialogflow_endpoint)).to eq('europe-west1-dialogflow.googleapis.com')
|
||||
expect(processor.send(:build_session_path, 'test-session')).to eq('projects/test-project/locations/europe-west1/agent/sessions/test-session')
|
||||
end
|
||||
end
|
||||
|
||||
it 'configures client with correct endpoint' do
|
||||
hook.update(settings: { 'project_id' => 'test', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
config = OpenStruct.new
|
||||
expect(Google::Cloud::Dialogflow::V2::Sessions::Client).to receive(:configure).and_yield(config)
|
||||
|
||||
processor.send(:configure_dialogflow_client_defaults)
|
||||
expect(config.endpoint).to eq('europe-west1-dialogflow.googleapis.com')
|
||||
end
|
||||
|
||||
context 'when calling detect_intent' do
|
||||
let(:mock_client) { instance_double(Google::Cloud::Dialogflow::V2::Sessions::Client) }
|
||||
|
||||
before do
|
||||
allow(Google::Cloud::Dialogflow::V2::Sessions::Client).to receive(:new).and_return(mock_client)
|
||||
end
|
||||
|
||||
it 'uses global session path when region is not specified' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {} })
|
||||
|
||||
expect(mock_client).to receive(:detect_intent).with(
|
||||
session: 'projects/test-project/agent/sessions/test-session',
|
||||
query_input: { text: { text: 'Hello', language_code: 'en-US' } }
|
||||
)
|
||||
|
||||
processor.send(:detect_intent, 'test-session', 'Hello')
|
||||
end
|
||||
|
||||
it 'uses regional session path when region is specified' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
|
||||
expect(mock_client).to receive(:detect_intent).with(
|
||||
session: 'projects/test-project/locations/europe-west1/agent/sessions/test-session',
|
||||
query_input: { text: { text: 'Hello', language_code: 'en-US' } }
|
||||
)
|
||||
|
||||
processor.send(:detect_intent, 'test-session', 'Hello')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,8 @@ describe CsatSurveyListener do
|
||||
)
|
||||
end
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
|
||||
let!(:csat_enabled_inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
|
||||
let!(:resolved_conversation) { create(:conversation, account: account, inbox: csat_enabled_inbox, status: :resolved) }
|
||||
|
||||
describe '#message_updated' do
|
||||
let(:event_name) { 'message.updated' }
|
||||
@@ -33,4 +35,33 @@ describe CsatSurveyListener do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#conversation_status_changed' do
|
||||
let(:event_name) { 'conversation.status_changed' }
|
||||
let(:csat_service) { double }
|
||||
|
||||
before do
|
||||
allow(resolved_conversation).to receive(:saved_change_to_status?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when conversation is resolved and CSAT is enabled' do
|
||||
it 'triggers CSAT survey service' do
|
||||
event = Events::Base.new(event_name, Time.zone.now, conversation: resolved_conversation)
|
||||
expect(csat_service).to receive(:perform)
|
||||
expect(CsatSurveyService).to receive(:new).with(conversation: resolved_conversation).and_return(csat_service)
|
||||
|
||||
listener.conversation_status_changed(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is not resolved' do
|
||||
it 'does not trigger CSAT survey service' do
|
||||
open_conversation = create(:conversation, account: account, inbox: csat_enabled_inbox, status: :open)
|
||||
event = Events::Base.new(event_name, Time.zone.now, conversation: open_conversation)
|
||||
|
||||
expect(CsatSurveyService).not_to receive(:new)
|
||||
listener.conversation_status_changed(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
|
||||
.display_id}] has been created in #{conversation.inbox&.name}.")
|
||||
.display_id}] has been created in #{conversation.inbox&.sanitized_name}.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
|
||||
@@ -87,7 +87,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
|
||||
|
||||
it 'has correct name' do
|
||||
expect(mail[:from].display_names).to eq(["#{message.sender.available_name} from Inbox"])
|
||||
expect(mail[:from].display_names).to eq(["#{message.sender.available_name} from #{message.conversation.inbox.sanitized_name}"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -232,11 +232,11 @@ RSpec.describe ConversationReplyMailer do
|
||||
end
|
||||
|
||||
context 'when smtp enabled for email channel' do
|
||||
let(:smtp_email_channel) do
|
||||
let(:smtp_channel) do
|
||||
create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com',
|
||||
smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account)
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -248,19 +248,19 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
it 'renders sender name in the from address' do
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name even when assignee is not present' do
|
||||
conversation.update(assignee_id: nil)
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders assignee name in the from address when sender_name not available' do
|
||||
message.update(sender_id: nil)
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders inbox name as sender and assignee or business_name not present' do
|
||||
@@ -268,7 +268,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: nil)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "Notifications from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "Notifications from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
context 'when friendly name enabled' do
|
||||
@@ -284,7 +284,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as sender and assignee nil and business_name present' do
|
||||
@@ -294,7 +294,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq(
|
||||
"Notifications from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
"Notifications from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -303,7 +303,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: agent.id)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as sender and assignee and business_name present' do
|
||||
@@ -312,7 +312,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: agent.id)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -329,7 +329,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as business_name present' do
|
||||
@@ -338,17 +338,17 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when smtp enabled for microsoft email channel' do
|
||||
let(:ms_smtp_email_channel) do
|
||||
let(:ms_smtp_channel) do
|
||||
create(:channel_email, imap_login: 'smtp@outlook.com',
|
||||
imap_enabled: true, account: account, provider: 'microsoft', provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -360,11 +360,11 @@ RSpec.describe ConversationReplyMailer do
|
||||
end
|
||||
|
||||
context 'when smtp enabled for google email channel' do
|
||||
let(:ms_smtp_email_channel) do
|
||||
let(:ms_smtp_channel) do
|
||||
create(:channel_email, imap_login: 'smtp@gmail.com',
|
||||
imap_enabled: true, account: account, provider: 'google', provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -438,7 +438,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
it 'sets reply to email to be based on the domain' do
|
||||
reply_to_email = "reply+#{message.conversation.uuid}@#{conversation.account.domain}"
|
||||
reply_to = "#{message.sender.available_name} from #{conversation.inbox.name} <#{reply_to_email}>"
|
||||
reply_to = "#{message.sender.available_name} from #{conversation.inbox.sanitized_name} <#{reply_to_email}>"
|
||||
expect(mail['REPLY-TO'].value).to eq(reply_to)
|
||||
expect(mail.reply_to).to eq([reply_to_email])
|
||||
end
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Conversation Audit', type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
before do
|
||||
# Enable auditing for conversations
|
||||
conversation.class.send(:include, Enterprise::Audit::Conversation) if defined?(Enterprise::Audit::Conversation)
|
||||
end
|
||||
|
||||
describe 'audit logging on destroy' do
|
||||
it 'creates an audit log when conversation is destroyed' do
|
||||
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
|
||||
|
||||
expect do
|
||||
conversation.destroy!
|
||||
end.to change(Audited::Audit, :count).by(1)
|
||||
|
||||
audit = Audited::Audit.last
|
||||
expect(audit.auditable_type).to eq('Conversation')
|
||||
expect(audit.action).to eq('destroy')
|
||||
expect(audit.auditable_id).to eq(conversation.id)
|
||||
end
|
||||
|
||||
it 'does not create audit log for other actions by default' do
|
||||
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
|
||||
|
||||
expect do
|
||||
conversation.update!(priority: 'high')
|
||||
end.not_to(change(Audited::Audit, :count))
|
||||
end
|
||||
end
|
||||
end
|
||||
+131
-25
@@ -164,31 +164,7 @@ RSpec.describe Inbox do
|
||||
let(:inbox) { FactoryBot.create(:inbox) }
|
||||
|
||||
context 'when validating inbox name' do
|
||||
it 'does not allow any special character at the end' do
|
||||
inbox.name = 'this is my inbox name-'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow any special character at the start' do
|
||||
inbox.name = '-this is my inbox name'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow chacters like /\@<> in the entire string' do
|
||||
inbox.name = 'inbox@name'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not empty string' do
|
||||
it 'does not allow empty string' do
|
||||
inbox.name = ''
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages[0]).to eq(
|
||||
@@ -282,4 +258,134 @@ RSpec.describe Inbox do
|
||||
inbox.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sanitized_name' do
|
||||
context 'when inbox name contains forbidden characters' do
|
||||
it 'removes forbidden and spam-trigger characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test/Name\\With<Bad>@Characters"And\'Quotes!#$%')
|
||||
expect(inbox.sanitized_name).to eq('Test/NameWithBadCharactersAnd\'Quotes')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has leading/trailing non-word characters' do
|
||||
it 'removes leading and trailing symbols' do
|
||||
inbox = FactoryBot.build(:inbox, name: '!!!Test Name***')
|
||||
expect(inbox.sanitized_name).to eq('Test Name')
|
||||
end
|
||||
|
||||
it 'handles mixed leading/trailing characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: '###@@@Test Inbox Name$$$%%')
|
||||
expect(inbox.sanitized_name).to eq('Test Inbox Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has multiple spaces' do
|
||||
it 'normalizes multiple spaces to single space' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test Multiple Spaces')
|
||||
expect(inbox.sanitized_name).to eq('Test Multiple Spaces')
|
||||
end
|
||||
|
||||
it 'handles tabs and other whitespace' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Test\t\nMultiple\r\nSpaces")
|
||||
expect(inbox.sanitized_name).to eq('Test Multiple Spaces')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has leading/trailing whitespace' do
|
||||
it 'strips whitespace' do
|
||||
inbox = FactoryBot.build(:inbox, name: ' Test Name ')
|
||||
expect(inbox.sanitized_name).to eq('Test Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name becomes empty after sanitization' do
|
||||
context 'with email channel' do
|
||||
it 'falls back to email local part' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'support@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '\\<>@"', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Support')
|
||||
end
|
||||
|
||||
it 'handles email with complex local part' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'help-desk_team@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '!!!@@@', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Help Desk Team')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-email channel' do
|
||||
it 'returns empty string when name becomes blank' do
|
||||
web_widget_channel = FactoryBot.build(:channel_widget)
|
||||
inbox = FactoryBot.build(:inbox, name: '\\<>@"', channel: web_widget_channel)
|
||||
expect(inbox.sanitized_name).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name is blank initially' do
|
||||
context 'with email channel' do
|
||||
it 'uses email local part as fallback' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'customer-care@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Customer Care')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-email channel' do
|
||||
it 'returns empty string' do
|
||||
api_channel = FactoryBot.build(:channel_api)
|
||||
inbox = FactoryBot.build(:inbox, name: '', channel: api_channel)
|
||||
expect(inbox.sanitized_name).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name contains valid characters' do
|
||||
it 'preserves valid characters like hyphens, underscores, and dots' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test-Name_With.Valid-Characters')
|
||||
expect(inbox.sanitized_name).to eq('Test-Name_With.Valid-Characters')
|
||||
end
|
||||
|
||||
it 'preserves alphanumeric characters and spaces' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Customer Support 123')
|
||||
expect(inbox.sanitized_name).to eq('Customer Support 123')
|
||||
end
|
||||
|
||||
it 'preserves balanced safe characters but removes spam-trigger symbols' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Test!#$%&'*+/=?^_`{|}~-Name")
|
||||
expect(inbox.sanitized_name).to eq("Test'/_-Name")
|
||||
end
|
||||
|
||||
it 'keeps commonly used safe characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Support/Help's Team.Desk_2024-Main")
|
||||
expect(inbox.sanitized_name).to eq("Support/Help's Team.Desk_2024-Main")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name contains problematic characters for email headers' do
|
||||
it 'preserves Unicode symbols (trademark, etc.)' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test™Name®With©Special™Characters')
|
||||
expect(inbox.sanitized_name).to eq('Test™Name®With©Special™Characters')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with edge cases' do
|
||||
it 'handles nil name gracefully' do
|
||||
inbox = FactoryBot.build(:inbox)
|
||||
allow(inbox).to receive(:name).and_return(nil)
|
||||
expect { inbox.sanitized_name }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'handles very long names' do
|
||||
long_name = 'A' * 1000
|
||||
inbox = FactoryBot.build(:inbox, name: long_name)
|
||||
expect(inbox.sanitized_name).to eq(long_name)
|
||||
end
|
||||
|
||||
it 'handles unicode characters and preserves emojis' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test Name with émojis 🎉')
|
||||
expect(inbox.sanitized_name).to eq('Test Name with émojis 🎉')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+44
-19
@@ -478,32 +478,57 @@ RSpec.describe Message do
|
||||
|
||||
describe '#content' do
|
||||
let(:conversation) { create(:conversation) }
|
||||
let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Original content') }
|
||||
|
||||
it 'returns original content for web widget inbox' do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(true)
|
||||
expect(message.content).to eq('Original content')
|
||||
context 'when message is not input_csat' do
|
||||
let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
|
||||
|
||||
it 'returns original content' do
|
||||
expect(message.content).to eq('Regular message')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox is not a web widget' do
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(false)
|
||||
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com')
|
||||
context 'when message is input_csat' do
|
||||
let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') }
|
||||
|
||||
context 'when inbox is web widget' do
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns original content without survey URL' do
|
||||
expect(message.content).to eq('Rate your experience')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns custom message with survey link when csat message is configured' do
|
||||
allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom survey message:' })
|
||||
expected_content = "Custom survey message: https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
|
||||
expect(message.content).to eq(expected_content)
|
||||
end
|
||||
context 'when inbox is not web widget' do
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns default message with survey link when no custom csat message' do
|
||||
allow(message.inbox).to receive(:csat_config).and_return(nil)
|
||||
allow(I18n).to receive(:t).with('conversations.survey.response', link: "https://app.chatwoot.com/survey/responses/#{conversation.uuid}")
|
||||
.and_return("Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}")
|
||||
expected_content = "Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
|
||||
expect(message.content).to eq(expected_content)
|
||||
it 'returns only the stored content (clean for dashboard)' do
|
||||
expect(message.content).to eq('Rate your experience')
|
||||
end
|
||||
|
||||
it 'returns only the base content without URL when survey_url stored separately' do
|
||||
message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' }
|
||||
expect(message.content).to eq('Rate your experience')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#outgoing_content' do
|
||||
let(:conversation) { create(:conversation) }
|
||||
let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
|
||||
|
||||
it 'delegates to MessageContentPresenter' do
|
||||
presenter = instance_double(MessageContentPresenter)
|
||||
allow(MessageContentPresenter).to receive(:new).with(message).and_return(presenter)
|
||||
allow(presenter).to receive(:outgoing_content).and_return('Presented content')
|
||||
|
||||
expect(message.outgoing_content).to eq('Presented content')
|
||||
expect(MessageContentPresenter).to have_received(:new).with(message)
|
||||
expect(presenter).to have_received(:outgoing_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ConversationPolicy, type: :policy do
|
||||
subject { described_class }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator_context) { { user: administrator, account: account, account_user: administrator.account_users.first } }
|
||||
let(:agent_context) { { user: agent, account: account, account_user: agent.account_users.first } }
|
||||
|
||||
permissions :destroy? do
|
||||
context 'when user is an administrator' do
|
||||
it 'allows destroy' do
|
||||
expect(subject).to permit(administrator_context, conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is an agent' do
|
||||
it 'denies destroy' do
|
||||
expect(subject).not_to permit(agent_context, conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :index? do
|
||||
context 'when user is authenticated' do
|
||||
it 'allows index' do
|
||||
expect(subject).to permit(agent_context, conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MessageContentPresenter do
|
||||
let(:conversation) { create(:conversation) }
|
||||
let(:message) { create(:message, conversation: conversation, content_type: content_type, content: content) }
|
||||
let(:presenter) { described_class.new(message) }
|
||||
|
||||
describe '#outgoing_content' do
|
||||
context 'when message is not input_csat' do
|
||||
let(:content_type) { 'text' }
|
||||
let(:content) { 'Regular message' }
|
||||
|
||||
it 'returns regular content' do
|
||||
expect(presenter.outgoing_content).to eq('Regular message')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is input_csat and inbox is web widget' do
|
||||
let(:content_type) { 'input_csat' }
|
||||
let(:content) { 'Rate your experience' }
|
||||
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns regular content without survey URL' do
|
||||
expect(presenter.outgoing_content).to eq('Rate your experience')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is input_csat and inbox is not web widget' do
|
||||
let(:content_type) { 'input_csat' }
|
||||
let(:content) { 'Rate your experience' }
|
||||
|
||||
before do
|
||||
allow(message.inbox).to receive(:web_widget?).and_return(false)
|
||||
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com')
|
||||
end
|
||||
|
||||
it 'returns I18n default message when no CSAT config and dynamically generates survey URL' do
|
||||
expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
|
||||
allow(I18n).to receive(:t).with('conversations.survey.response', link: expected_url)
|
||||
.and_return("Please rate this conversation, #{expected_url}")
|
||||
expect(presenter.outgoing_content).to eq("Please rate this conversation, #{expected_url}")
|
||||
end
|
||||
|
||||
it 'returns CSAT config message when config exists and dynamically generates survey URL' do
|
||||
allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom CSAT message' })
|
||||
expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
|
||||
expect(presenter.outgoing_content).to eq("Custom CSAT message #{expected_url}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'delegation' do
|
||||
let(:content_type) { 'text' }
|
||||
let(:content) { 'Test message' }
|
||||
|
||||
it 'delegates model methods to the wrapped message' do
|
||||
expect(presenter.content).to eq('Test message')
|
||||
expect(presenter.content_type).to eq('text')
|
||||
expect(presenter.conversation).to eq(conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CsatSurveyService do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
|
||||
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :resolved) }
|
||||
let(:service) { described_class.new(conversation: conversation) }
|
||||
|
||||
describe '#perform' do
|
||||
let(:csat_template) { instance_double(MessageTemplates::Template::CsatSurvey) }
|
||||
|
||||
before do
|
||||
allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_template)
|
||||
allow(csat_template).to receive(:perform)
|
||||
allow(Conversations::ActivityMessageJob).to receive(:perform_later)
|
||||
end
|
||||
|
||||
context 'when CSAT survey should be sent' do
|
||||
before do
|
||||
allow(conversation).to receive(:can_reply?).and_return(true)
|
||||
end
|
||||
|
||||
it 'sends CSAT survey when within messaging window' do
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_template).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when outside messaging window' do
|
||||
before do
|
||||
allow(conversation).to receive(:can_reply?).and_return(false)
|
||||
end
|
||||
|
||||
it 'creates activity message instead of sending survey' do
|
||||
service.perform
|
||||
|
||||
expect(Conversations::ActivityMessageJob).to have_received(:perform_later).with(
|
||||
conversation,
|
||||
hash_including(content: I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window'))
|
||||
)
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CSAT survey should not be sent' do
|
||||
it 'does nothing when conversation is not resolved' do
|
||||
conversation.update(status: :open)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing when CSAT survey is not enabled' do
|
||||
inbox.update(csat_survey_enabled: false)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing when CSAT already sent' do
|
||||
create(:message, conversation: conversation, content_type: :input_csat)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
|
||||
it 'does nothing for Twitter conversations' do
|
||||
twitter_channel = create(:channel_twitter_profile)
|
||||
twitter_inbox = create(:inbox, channel: twitter_channel, csat_survey_enabled: true)
|
||||
twitter_conversation = create(:conversation,
|
||||
inbox: twitter_inbox,
|
||||
status: :resolved,
|
||||
additional_attributes: { type: 'tweet' })
|
||||
twitter_service = described_class.new(conversation: twitter_conversation)
|
||||
|
||||
twitter_service.perform
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
|
||||
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -166,5 +166,33 @@ describe Facebook::SendOnFacebookService do
|
||||
expect(message.status).not_to eq('failed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with input_select' do
|
||||
it 'if message with input_select is sent from chatwoot and is outgoing' do
|
||||
message = build(
|
||||
:message,
|
||||
message_type: 'outgoing',
|
||||
inbox: facebook_inbox,
|
||||
account: account,
|
||||
conversation: conversation,
|
||||
content_type: 'input_select',
|
||||
content_attributes: { 'items' => [{ 'title' => 'text 1', 'value' => 'value 1' }, { 'title' => 'text 2', 'value' => 'value 2' }] }
|
||||
)
|
||||
|
||||
described_class.new(message: message).perform
|
||||
expect(bot).to have_received(:deliver).with({
|
||||
recipient: { id: contact_inbox.source_id },
|
||||
message: {
|
||||
text: message.content,
|
||||
quick_replies: [
|
||||
{ content_type: 'text', payload: 'text 1', title: 'text 1' },
|
||||
{ content_type: 'text', payload: 'text 2', title: 'text 2' }
|
||||
]
|
||||
},
|
||||
messaging_type: 'MESSAGE_TAG',
|
||||
tag: 'ACCOUNT_UPDATE'
|
||||
}, { page_id: facebook_channel.page_id })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,6 +93,69 @@ describe Line::SendOnLineService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with message input_select' do
|
||||
let(:success_response) do
|
||||
{
|
||||
'message' => 'ok'
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:expect_message) do
|
||||
{
|
||||
type: 'flex',
|
||||
altText: 'test',
|
||||
contents: {
|
||||
type: 'bubble',
|
||||
body: {
|
||||
type: 'box',
|
||||
layout: 'vertical',
|
||||
contents: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'test'
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
style: 'link',
|
||||
height: 'sm',
|
||||
action: {
|
||||
type: 'message',
|
||||
label: 'text 1',
|
||||
text: 'value 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
style: 'link',
|
||||
height: 'sm',
|
||||
action: {
|
||||
type: 'message',
|
||||
label: 'text 2',
|
||||
text: 'value 2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'sends the message with input_select' do
|
||||
message = create(
|
||||
:message, message_type: :outgoing, content: 'test', content_type: 'input_select',
|
||||
content_attributes: { 'items' => [{ 'title' => 'text 1', 'value' => 'value 1' }, { 'title' => 'text 2', 'value' => 'value 2' }] },
|
||||
conversation: create(:conversation, inbox: line_channel.inbox)
|
||||
)
|
||||
|
||||
expect(line_client).to receive(:push_message).with(
|
||||
message.conversation.contact_inbox.source_id,
|
||||
expect_message
|
||||
).and_return(OpenStruct.new(code: '200', body: success_response))
|
||||
|
||||
described_class.new(message: message).perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'with message attachments' do
|
||||
it 'sends the message with text and attachments' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Liquid::CampaignTemplateService do
|
||||
subject(:template_service) { described_class.new(campaign: campaign, contact: contact) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account, name: 'John Doe', phone_number: '+1234567890') }
|
||||
let(:campaign) { create(:campaign, account: account, inbox: inbox, sender: agent, message: message_content) }
|
||||
|
||||
describe '#call' do
|
||||
context 'with liquid template variables' do
|
||||
let(:message_content) { 'Hello {{contact.name}}, this is {{agent.name}} from {{account.name}}' }
|
||||
|
||||
it 'processes liquid template correctly' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to eq("Hello #{contact_drop_name}, this is #{agent_drop_name} from #{account.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with code blocks' do
|
||||
let(:message_content) { 'Check this code: `const x = {{contact.name}}`' }
|
||||
|
||||
it 'preserves code blocks without processing liquid' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to include('`const x = {{contact.name}}`')
|
||||
expect(result).not_to include(contact.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiline code blocks' do
|
||||
let(:message_content) do
|
||||
<<~MESSAGE
|
||||
Here's some code:
|
||||
```
|
||||
function greet() {
|
||||
return "Hello {{contact.name}}";
|
||||
}
|
||||
```
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
it 'preserves multiline code blocks without processing liquid' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to include('{{contact.name}}')
|
||||
expect(result).not_to include(contact.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with malformed liquid syntax' do
|
||||
let(:message_content) { 'Hello {{contact.name missing closing braces' }
|
||||
|
||||
it 'returns original message when liquid parsing fails' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to eq(message_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid liquid tags' do
|
||||
let(:message_content) { 'Hello {% invalid_tag %} world' }
|
||||
|
||||
it 'returns original message when liquid parsing fails' do
|
||||
result = template_service.call(message_content)
|
||||
|
||||
expect(result).to eq(message_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mixed content' do
|
||||
let(:message_content) { 'Hi {{contact.name}}, use this code: `{{agent.name}}` to contact {{agent.name}}' }
|
||||
|
||||
it 'processes liquid outside code blocks but preserves code blocks' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to include("Hi #{contact_drop_name}")
|
||||
expect(result).to include("contact #{agent_drop_name}")
|
||||
expect(result).to include('`{{agent.name}}`')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with all drop types' do
|
||||
let(:message_content) do
|
||||
'Contact: {{contact.name}}, Agent: {{agent.name}}, Inbox: {{inbox.name}}, Account: {{account.name}}'
|
||||
end
|
||||
|
||||
it 'processes all available drops' do
|
||||
result = template_service.call(message_content)
|
||||
agent_drop_name = UserDrop.new(agent).name
|
||||
contact_drop_name = ContactDrop.new(contact).name
|
||||
|
||||
expect(result).to include("Contact: #{contact_drop_name}")
|
||||
expect(result).to include("Agent: #{agent_drop_name}")
|
||||
expect(result).to include("Inbox: #{inbox.name}")
|
||||
expect(result).to include("Account: #{account.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -111,69 +111,6 @@ describe MessageTemplates::HookExecutionService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CSAT Survey' do
|
||||
let(:csat_survey) { double }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
before do
|
||||
allow(MessageTemplates::Template::CsatSurvey).to receive(:new).and_return(csat_survey)
|
||||
allow(csat_survey).to receive(:perform).and_return(true)
|
||||
create(:message, conversation: conversation, message_type: 'incoming')
|
||||
end
|
||||
|
||||
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey when Csat is not enabled' do
|
||||
conversation.inbox.update(csat_survey_enabled: false)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey if its a tweet conversation' do
|
||||
twitter_channel = create(:channel_twitter_profile)
|
||||
twitter_inbox = create(:inbox, channel: twitter_channel)
|
||||
conversation = create(:conversation, inbox: twitter_inbox, additional_attributes: { type: 'tweet' })
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey if another Csat was already sent' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
conversation.messages.create!(message_type: 'outgoing', content_type: :input_csat, account: conversation.account, inbox: conversation.inbox)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is after working hours' do
|
||||
it 'calls ::MessageTemplates::Template::OutOfOffice' do
|
||||
contact = create(:contact)
|
||||
|
||||
@@ -43,5 +43,14 @@ describe Sms::OneoffSmsCampaignService do
|
||||
assert_requested(:post, 'https://messaging.bandwidth.com/api/v2/users/1/messages', times: 3)
|
||||
expect(campaign.reload.completed?).to be true
|
||||
end
|
||||
|
||||
it 'uses liquid template service to process campaign message' do
|
||||
contact = create(:contact, :with_phone_number, account: account)
|
||||
contact.update_labels([label1.title])
|
||||
|
||||
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
|
||||
|
||||
sms_campaign_service.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -179,6 +179,35 @@ describe Telegram::IncomingMessageService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid video_note messages params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mov')
|
||||
params = {
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'message' => {
|
||||
'video_note' => {
|
||||
'duration' => 3,
|
||||
'length' => 240,
|
||||
'thumb' => {
|
||||
'file_id' => 'AAMCBQADGQEAA4ZhXd78Xz6_c6gCzbdIkgGiXJcwwwACqwMAAp3x8Fbhf3EWamgCWAEAB20AAyEE',
|
||||
'file_unique_id' => 'AQADqwMAAp3x8FZy',
|
||||
'file_size' => 11_462,
|
||||
'width' => 240,
|
||||
'height' => 240
|
||||
},
|
||||
'file_id' => 'DQACAgUAAxkBAAIBY2FdJlhf8PC2E3IalXSvXWO5m8GBAALJAwACwqHgVhb0truM0uhwIQQ',
|
||||
'file_unique_id' => 'AgADyQMAAsKh4FY',
|
||||
'file_size' => 132_446
|
||||
}
|
||||
}.merge(message_params)
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid voice attachment params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.ogg')
|
||||
|
||||
@@ -60,5 +60,15 @@ describe Twilio::OneoffSmsCampaignService do
|
||||
sms_campaign_service.perform
|
||||
expect(campaign.reload.completed?).to be true
|
||||
end
|
||||
|
||||
it 'uses liquid template service to process campaign message' do
|
||||
contact = create(:contact, :with_phone_number, account: account)
|
||||
contact.update_labels([label1.title])
|
||||
|
||||
expect(Liquid::CampaignTemplateService).to receive(:new).with(campaign: campaign, contact: contact).and_call_original
|
||||
expect(twilio_messages).to receive(:create).once
|
||||
|
||||
sms_campaign_service.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,16 +51,15 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
|
||||
end
|
||||
|
||||
it 'calls message endpoints with list payload when number of items is greater than 3' do
|
||||
items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
|
||||
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
|
||||
content_type: 'input_select',
|
||||
content_attributes: {
|
||||
items: [
|
||||
{ title: 'Burito', value: 'Burito' },
|
||||
{ title: 'Pasta', value: 'Pasta' },
|
||||
{ title: 'Sushi', value: 'Sushi' },
|
||||
{ title: 'Salad', value: 'Salad' }
|
||||
]
|
||||
})
|
||||
content_type: 'input_select', content_attributes: { items: items })
|
||||
|
||||
expected_action = {
|
||||
button: I18n.t('conversations.messages.whatsapp.list_button_label'),
|
||||
sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
|
||||
}.to_json
|
||||
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/messages')
|
||||
.with(
|
||||
body: {
|
||||
@@ -70,9 +69,9 @@ describe Whatsapp::Providers::Whatsapp360DialogService do
|
||||
body: {
|
||||
text: 'test'
|
||||
},
|
||||
action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
|
||||
'{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
|
||||
}, type: 'interactive'
|
||||
action: expected_action
|
||||
},
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
|
||||
@@ -124,16 +124,15 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
end
|
||||
|
||||
it 'calls message endpoints with list payload when number of items is greater than 3' do
|
||||
items = %w[Burito Pasta Sushi Salad].map { |i| { title: i, value: i } }
|
||||
message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
|
||||
content_type: 'input_select',
|
||||
content_attributes: {
|
||||
items: [
|
||||
{ title: 'Burito', value: 'Burito' },
|
||||
{ title: 'Pasta', value: 'Pasta' },
|
||||
{ title: 'Sushi', value: 'Sushi' },
|
||||
{ title: 'Salad', value: 'Salad' }
|
||||
]
|
||||
})
|
||||
content_type: 'input_select', content_attributes: { items: items })
|
||||
|
||||
expected_action = {
|
||||
button: I18n.t('conversations.messages.whatsapp.list_button_label'),
|
||||
sections: [{ rows: %w[Burito Pasta Sushi Salad].map { |i| { id: i, title: i } } }]
|
||||
}.to_json
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||
.with(
|
||||
body: {
|
||||
@@ -143,9 +142,9 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
body: {
|
||||
text: 'test'
|
||||
},
|
||||
action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
|
||||
'{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
|
||||
}, type: 'interactive'
|
||||
action: expected_action
|
||||
},
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||
|
||||
Reference in New Issue
Block a user