Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
- Add Logistics component with progress tracking - Add OrderDetail component for order information - Support data-driven steps and actions - Add blue color scale to widget SCSS - Fix node overflow and progress bar rendering issues - Add English translations for dashboard components Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
127
spec/jobs/account/contacts_export_job_spec.rb
Normal file
127
spec/jobs/account/contacts_export_job_spec.rb
Normal file
@@ -0,0 +1,127 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Account::ContactsExportJob do
|
||||
subject(:job) { described_class.perform_later(account.id, user.id, [], {}) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account, email: 'account-user-test@test.com') }
|
||||
let(:label) { create(:label, title: 'spec-billing', maccount: account) }
|
||||
|
||||
let(:email_filter) do
|
||||
{
|
||||
:attribute_key => 'email',
|
||||
:filter_operator => 'contains',
|
||||
:values => 'looped',
|
||||
:query_operator => 'and',
|
||||
:attribute_model => 'standard',
|
||||
:custom_attribute_type => ''
|
||||
}
|
||||
end
|
||||
|
||||
let(:city_filter) do
|
||||
{
|
||||
:attribute_key => 'country_code',
|
||||
:filter_operator => 'equal_to',
|
||||
:values => ['India'],
|
||||
:query_operator => 'and',
|
||||
:attribute_model => 'standard',
|
||||
:custom_attribute_type => ''
|
||||
}
|
||||
end
|
||||
|
||||
let(:single_filter) do
|
||||
{
|
||||
:payload => [email_filter.merge(:query_operator => nil)]
|
||||
}
|
||||
end
|
||||
|
||||
let(:multiple_filters) do
|
||||
{
|
||||
:payload => [city_filter, email_filter.merge(:query_operator => nil)]
|
||||
}
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when export_contacts' do
|
||||
before do
|
||||
create(:contact, account: account, phone_number: '+910808080818', email: 'test1@text.example')
|
||||
4.times do |i|
|
||||
create(:contact, account: account, email: "looped-#{i + 3}@text.example.com")
|
||||
end
|
||||
4.times do |i|
|
||||
create(:contact, account: account, additional_attributes: { :country_code => 'India' }, email: "looped-#{i + 10}@text.example.com")
|
||||
end
|
||||
create(:contact, account: account, phone_number: '+910808080808', email: 'test2@text.example')
|
||||
end
|
||||
|
||||
it 'generates CSV file and attach to account' do
|
||||
mailer = double
|
||||
allow(AdministratorNotifications::AccountNotificationMailer).to receive(:with).with(account: account).and_return(mailer)
|
||||
allow(mailer).to receive(:contact_export_complete)
|
||||
|
||||
described_class.perform_now(account.id, user.id, [], {})
|
||||
|
||||
file_url = Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export)
|
||||
|
||||
expect(account.contacts_export).to be_present
|
||||
expect(file_url).to be_present
|
||||
expect(mailer).to have_received(:contact_export_complete).with(file_url, user.email)
|
||||
end
|
||||
|
||||
it 'generates valid data export file' do
|
||||
described_class.perform_now(account.id, user.id, %w[id name email phone_number column_not_present], {})
|
||||
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
emails = csv_data.pluck('email')
|
||||
phone_numbers = csv_data.pluck('phone_number')
|
||||
|
||||
expect(csv_data.length).to eq(account.contacts.count)
|
||||
|
||||
expect(emails).to include('test1@text.example', 'test2@text.example')
|
||||
expect(phone_numbers).to include('+910808080818', '+910808080808')
|
||||
end
|
||||
|
||||
it 'returns all resolved contacts as results when filter is not prvoided' do
|
||||
create(:contact, account: account, email: nil, phone_number: nil)
|
||||
described_class.perform_now(account.id, user.id, %w[id name email column_not_present], {})
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
expect(csv_data.length).to eq(account.contacts.resolved_contacts.count)
|
||||
end
|
||||
|
||||
it 'returns resolved contacts filtered if labels are provided' do
|
||||
# Adding label to a resolved contact
|
||||
Contact.last.add_labels(['spec-billing'])
|
||||
contact = create(:contact, account: account, email: nil, phone_number: nil)
|
||||
contact.add_labels(['spec-billing'])
|
||||
described_class.perform_now(account.id, user.id, [], { :payload => nil, :label => 'spec-billing' })
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
# since there is only 1 resolved contact with 'spec-billing' label
|
||||
expect(csv_data.length).to eq(1)
|
||||
end
|
||||
|
||||
it 'returns filtered data limited to resolved contacts when filter is provided' do
|
||||
create(:contact, account: account, email: nil, phone_number: nil, additional_attributes: { :country_code => 'India' })
|
||||
described_class.perform_now(account.id, user.id, [], { :payload => [city_filter.merge(:query_operator => nil)] }.with_indifferent_access)
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
expect(csv_data.length).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns filtered data when multiple filters are provided' do
|
||||
described_class.perform_now(account.id, user.id, [], multiple_filters.with_indifferent_access)
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
# since there are only 4 contacts with 'looped' in email and 'India' as country_code
|
||||
expect(csv_data.length).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns filtered data when a single filter is provided' do
|
||||
described_class.perform_now(account.id, user.id, [], single_filter.with_indifferent_access)
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
# since there are only 8 contacts with 'looped' in email
|
||||
expect(csv_data.length).to eq(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Account::ConversationsResolutionSchedulerJob do
|
||||
subject(:job) { described_class.perform_later }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
it 'enqueues Conversations::ResolutionJob' do
|
||||
account.update(auto_resolve_after: 10 * 60 * 24)
|
||||
expect(Conversations::ResolutionJob).to receive(:perform_later).with(account: account).once
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
22
spec/jobs/agent_bots/webhook_job_spec.rb
Normal file
22
spec/jobs/agent_bots/webhook_job_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AgentBots::WebhookJob do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
subject(:job) { described_class.perform_later(url, payload, webhook_type) }
|
||||
|
||||
let(:url) { 'https://test.com' }
|
||||
let(:payload) { { name: 'test' } }
|
||||
let(:webhook_type) { :agent_bot_webhook }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(url, payload, webhook_type)
|
||||
.on_queue('high')
|
||||
end
|
||||
|
||||
it 'executes perform' do
|
||||
expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type)
|
||||
perform_enqueued_jobs { job }
|
||||
end
|
||||
end
|
||||
34
spec/jobs/agents/destroy_job_spec.rb
Normal file
34
spec/jobs/agents/destroy_job_spec.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Agents::DestroyJob do
|
||||
subject(:job) { described_class.perform_later(account, user) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:team1) { create(:team, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create(:team_member, team: team1, user: user)
|
||||
create(:inbox_member, inbox: inbox, user: user)
|
||||
create(:conversation, account: account, assignee: user, inbox: inbox)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(account, user)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'remove inboxes, teams, and conversations when removed from account' do
|
||||
described_class.perform_now(account, user)
|
||||
|
||||
user.reload
|
||||
expect(user.teams.length).to eq 0
|
||||
expect(user.inboxes.length).to eq 0
|
||||
expect(user.notification_settings.length).to eq 0
|
||||
expect(user.assigned_conversations.where(account: account).length).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/jobs/auto_assignment/assignment_job_spec.rb
Normal file
85
spec/jobs/auto_assignment/assignment_job_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AutoAssignment::AssignmentJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
|
||||
let(:agent) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, inbox: inbox, user: agent)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when inbox exists' do
|
||||
context 'when auto assignment is enabled' do
|
||||
it 'calls the assignment service' do
|
||||
service = instance_double(AutoAssignment::AssignmentService)
|
||||
allow(AutoAssignment::AssignmentService).to receive(:new).with(inbox: inbox).and_return(service)
|
||||
expect(service).to receive(:perform_bulk_assignment).with(limit: 100).and_return(5)
|
||||
|
||||
described_class.new.perform(inbox_id: inbox.id)
|
||||
end
|
||||
|
||||
it 'logs the assignment count' do
|
||||
service = instance_double(AutoAssignment::AssignmentService)
|
||||
allow(AutoAssignment::AssignmentService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:perform_bulk_assignment).and_return(3)
|
||||
|
||||
expect(Rails.logger).to receive(:info).with("Assigned 3 conversations for inbox #{inbox.id}")
|
||||
|
||||
described_class.new.perform(inbox_id: inbox.id)
|
||||
end
|
||||
|
||||
it 'uses custom bulk limit from environment' do
|
||||
allow(ENV).to receive(:fetch).with('AUTO_ASSIGNMENT_BULK_LIMIT', 100).and_return('50')
|
||||
|
||||
service = instance_double(AutoAssignment::AssignmentService)
|
||||
allow(AutoAssignment::AssignmentService).to receive(:new).with(inbox: inbox).and_return(service)
|
||||
expect(service).to receive(:perform_bulk_assignment).with(limit: 50).and_return(2)
|
||||
|
||||
described_class.new.perform(inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when auto assignment is disabled' do
|
||||
before { inbox.update!(enable_auto_assignment: false) }
|
||||
|
||||
it 'calls the service which handles the disabled state' do
|
||||
service = instance_double(AutoAssignment::AssignmentService)
|
||||
allow(AutoAssignment::AssignmentService).to receive(:new).with(inbox: inbox).and_return(service)
|
||||
expect(service).to receive(:perform_bulk_assignment).with(limit: 100).and_return(0)
|
||||
|
||||
described_class.new.perform(inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox does not exist' do
|
||||
it 'returns early without processing' do
|
||||
expect(AutoAssignment::AssignmentService).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(inbox_id: 999_999)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an error occurs' do
|
||||
it 'logs the error and re-raises in test environment' do
|
||||
service = instance_double(AutoAssignment::AssignmentService)
|
||||
allow(AutoAssignment::AssignmentService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:perform_bulk_assignment).and_raise(StandardError, 'Something went wrong')
|
||||
|
||||
expect(Rails.logger).to receive(:error).with("Bulk assignment failed for inbox #{inbox.id}: Something went wrong")
|
||||
|
||||
expect do
|
||||
described_class.new.perform(inbox_id: inbox.id)
|
||||
end.to raise_error(StandardError, 'Something went wrong')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'job configuration' do
|
||||
it 'is queued in the default queue' do
|
||||
expect(described_class.queue_name).to eq('default')
|
||||
end
|
||||
end
|
||||
end
|
||||
123
spec/jobs/auto_assignment/periodic_assignment_job_spec.rb
Normal file
123
spec/jobs/auto_assignment/periodic_assignment_job_spec.rb
Normal file
@@ -0,0 +1,123 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AutoAssignment::PeriodicAssignmentJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account) }
|
||||
let(:inbox_assignment_policy) { create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, inbox: inbox, user: agent)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when account has assignment_v2 feature enabled' do
|
||||
before do
|
||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
||||
allow(Account).to receive(:find_in_batches).and_yield([account])
|
||||
end
|
||||
|
||||
context 'when inbox has auto_assignment_v2 enabled' do
|
||||
before do
|
||||
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
||||
inbox_relation = instance_double(ActiveRecord::Relation)
|
||||
allow(account).to receive(:inboxes).and_return(inbox_relation)
|
||||
allow(inbox_relation).to receive(:joins).with(:assignment_policy).and_return(inbox_relation)
|
||||
allow(inbox_relation).to receive(:find_in_batches).and_yield([inbox])
|
||||
end
|
||||
|
||||
it 'queues assignment job for eligible inboxes' do
|
||||
inbox_assignment_policy # ensure it exists
|
||||
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox.id)
|
||||
|
||||
described_class.new.perform
|
||||
end
|
||||
|
||||
it 'processes multiple accounts' do
|
||||
inbox_assignment_policy # ensure it exists
|
||||
account2 = create(:account)
|
||||
inbox2 = create(:inbox, account: account2, enable_auto_assignment: true)
|
||||
policy2 = create(:assignment_policy, account: account2)
|
||||
create(:inbox_assignment_policy, inbox: inbox2, assignment_policy: policy2)
|
||||
|
||||
allow(account2).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
||||
allow(inbox2).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
||||
|
||||
inbox_relation2 = instance_double(ActiveRecord::Relation)
|
||||
allow(account2).to receive(:inboxes).and_return(inbox_relation2)
|
||||
allow(inbox_relation2).to receive(:joins).with(:assignment_policy).and_return(inbox_relation2)
|
||||
allow(inbox_relation2).to receive(:find_in_batches).and_yield([inbox2])
|
||||
|
||||
allow(Account).to receive(:find_in_batches).and_yield([account]).and_yield([account2])
|
||||
|
||||
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox.id)
|
||||
expect(AutoAssignment::AssignmentJob).to receive(:perform_later).with(inbox_id: inbox2.id)
|
||||
|
||||
described_class.new.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox does not have auto_assignment_v2 enabled' do
|
||||
before do
|
||||
allow(inbox).to receive(:auto_assignment_v2_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not queue assignment job' do
|
||||
expect(AutoAssignment::AssignmentJob).not_to receive(:perform_later)
|
||||
|
||||
described_class.new.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account does not have assignment_v2 feature enabled' do
|
||||
before do
|
||||
allow(account).to receive(:feature_enabled?).with('assignment_v2').and_return(false)
|
||||
allow(Account).to receive(:find_in_batches).and_yield([account])
|
||||
end
|
||||
|
||||
it 'does not process the account' do
|
||||
expect(AutoAssignment::AssignmentJob).not_to receive(:perform_later)
|
||||
|
||||
described_class.new.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'with batch processing' do
|
||||
it 'processes accounts in batches' do
|
||||
accounts = []
|
||||
# Create multiple accounts
|
||||
5.times do |_i|
|
||||
acc = create(:account)
|
||||
inb = create(:inbox, account: acc, enable_auto_assignment: true)
|
||||
policy = create(:assignment_policy, account: acc)
|
||||
create(:inbox_assignment_policy, inbox: inb, assignment_policy: policy)
|
||||
allow(acc).to receive(:feature_enabled?).with('assignment_v2').and_return(true)
|
||||
allow(inb).to receive(:auto_assignment_v2_enabled?).and_return(true)
|
||||
|
||||
inbox_relation = instance_double(ActiveRecord::Relation)
|
||||
allow(acc).to receive(:inboxes).and_return(inbox_relation)
|
||||
allow(inbox_relation).to receive(:joins).with(:assignment_policy).and_return(inbox_relation)
|
||||
allow(inbox_relation).to receive(:find_in_batches).and_yield([inb])
|
||||
|
||||
accounts << acc
|
||||
end
|
||||
|
||||
allow(Account).to receive(:find_in_batches) do |&block|
|
||||
accounts.each { |acc| block.call([acc]) }
|
||||
end
|
||||
|
||||
expect(Account).to receive(:find_in_batches).and_call_original
|
||||
|
||||
described_class.new.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'job configuration' do
|
||||
it 'is queued in the scheduled_jobs queue' do
|
||||
expect(described_class.queue_name).to eq('scheduled_jobs')
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/jobs/avatar/avatar_from_gravatar_job_spec.rb
Normal file
29
spec/jobs/avatar/avatar_from_gravatar_job_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Avatar::AvatarFromGravatarJob do
|
||||
let(:avatarable) { create(:contact) }
|
||||
let(:email) { 'test@test.com' }
|
||||
let(:gravatar_url) { "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}?d=404" }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later(avatarable, email) }.to have_enqueued_job(described_class)
|
||||
.on_queue('purgable')
|
||||
end
|
||||
|
||||
it 'will call AvatarFromUrlJob with gravatar url' do
|
||||
expect(Avatar::AvatarFromUrlJob).to receive(:perform_later).with(avatarable, gravatar_url)
|
||||
described_class.perform_now(avatarable, email)
|
||||
end
|
||||
|
||||
it 'will not call AvatarFromUrlJob if DISABLE_GRAVATAR is configured' do
|
||||
with_modified_env DISABLE_GRAVATAR: 'true' do
|
||||
expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later).with(avatarable, gravatar_url)
|
||||
described_class.perform_now(avatarable, '')
|
||||
end
|
||||
end
|
||||
|
||||
it 'will not call AvatarFromUrlJob if email is blank' do
|
||||
expect(Avatar::AvatarFromUrlJob).not_to receive(:perform_later).with(avatarable, gravatar_url)
|
||||
described_class.perform_now(avatarable, '')
|
||||
end
|
||||
end
|
||||
119
spec/jobs/avatar/avatar_from_url_job_spec.rb
Normal file
119
spec/jobs/avatar/avatar_from_url_job_spec.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Avatar::AvatarFromUrlJob do
|
||||
let(:file) { fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') }
|
||||
let(:valid_url) { 'https://example.com/avatar.png' }
|
||||
|
||||
it 'enqueues the job' do
|
||||
contact = create(:contact)
|
||||
expect { described_class.perform_later(contact, 'https://example.com/avatar.png') }
|
||||
.to have_enqueued_job(described_class).on_queue('purgable')
|
||||
end
|
||||
|
||||
context 'with rate-limited avatarable (Contact)' do
|
||||
let(:avatarable) { create(:contact) }
|
||||
|
||||
it 'attaches and updates sync attributes' do
|
||||
expect(Down).to receive(:download).with(valid_url, max_size: Avatar::AvatarFromUrlJob::MAX_DOWNLOAD_SIZE).and_return(file)
|
||||
described_class.perform_now(avatarable, valid_url)
|
||||
avatarable.reload
|
||||
expect(avatarable.avatar).to be_attached
|
||||
expect(avatarable.additional_attributes['avatar_url_hash']).to eq(Digest::SHA256.hexdigest(valid_url))
|
||||
expect(avatarable.additional_attributes['last_avatar_sync_at']).to be_present
|
||||
end
|
||||
|
||||
it 'returns early when rate limited' do
|
||||
ts = 30.seconds.ago.iso8601
|
||||
avatarable.update(additional_attributes: { 'last_avatar_sync_at' => ts })
|
||||
expect(Down).not_to receive(:download)
|
||||
described_class.perform_now(avatarable, valid_url)
|
||||
avatarable.reload
|
||||
expect(avatarable.avatar).not_to be_attached
|
||||
expect(avatarable.additional_attributes['last_avatar_sync_at']).to be_present
|
||||
expect(Time.zone.parse(avatarable.additional_attributes['last_avatar_sync_at']))
|
||||
.to be > Time.zone.parse(ts)
|
||||
expect(avatarable.additional_attributes['avatar_url_hash']).to eq(Digest::SHA256.hexdigest(valid_url))
|
||||
end
|
||||
|
||||
it 'returns early when hash unchanged' do
|
||||
avatarable.update(additional_attributes: { 'avatar_url_hash' => Digest::SHA256.hexdigest(valid_url) })
|
||||
expect(Down).not_to receive(:download)
|
||||
described_class.perform_now(avatarable, valid_url)
|
||||
expect(avatarable.avatar).not_to be_attached
|
||||
avatarable.reload
|
||||
expect(avatarable.additional_attributes['last_avatar_sync_at']).to be_present
|
||||
expect(avatarable.additional_attributes['avatar_url_hash']).to eq(Digest::SHA256.hexdigest(valid_url))
|
||||
end
|
||||
|
||||
it 'updates sync attributes even when URL is invalid' do
|
||||
invalid_url = 'invalid_url'
|
||||
expect(Down).not_to receive(:download)
|
||||
described_class.perform_now(avatarable, invalid_url)
|
||||
avatarable.reload
|
||||
expect(avatarable.avatar).not_to be_attached
|
||||
expect(avatarable.additional_attributes['last_avatar_sync_at']).to be_present
|
||||
expect(avatarable.additional_attributes['avatar_url_hash']).to eq(Digest::SHA256.hexdigest(invalid_url))
|
||||
end
|
||||
|
||||
it 'updates sync attributes when file download is valid but content type is unsupported' do
|
||||
temp_file = Tempfile.new(['invalid', '.xml'])
|
||||
temp_file.write('<invalid>content</invalid>')
|
||||
temp_file.rewind
|
||||
|
||||
uploaded = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile: temp_file,
|
||||
filename: 'invalid.xml',
|
||||
type: 'application/xml'
|
||||
)
|
||||
|
||||
expect(Down).to receive(:download).with(valid_url, max_size: Avatar::AvatarFromUrlJob::MAX_DOWNLOAD_SIZE).and_return(uploaded)
|
||||
|
||||
described_class.perform_now(avatarable, valid_url)
|
||||
avatarable.reload
|
||||
|
||||
expect(avatarable.avatar).not_to be_attached
|
||||
expect(avatarable.additional_attributes['last_avatar_sync_at']).to be_present
|
||||
expect(avatarable.additional_attributes['avatar_url_hash']).to eq(Digest::SHA256.hexdigest(valid_url))
|
||||
|
||||
temp_file.close
|
||||
temp_file.unlink
|
||||
end
|
||||
end
|
||||
|
||||
context 'with regular avatarable' do
|
||||
let(:avatarable) { create(:agent_bot) }
|
||||
|
||||
it 'downloads and attaches avatar' do
|
||||
expect(Down).to receive(:download).with(valid_url, max_size: Avatar::AvatarFromUrlJob::MAX_DOWNLOAD_SIZE).and_return(file)
|
||||
described_class.perform_now(avatarable, valid_url)
|
||||
expect(avatarable.avatar).to be_attached
|
||||
end
|
||||
end
|
||||
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/10449
|
||||
it 'does not raise error when downloaded file has no filename (invalid content)' do
|
||||
contact = create(:contact)
|
||||
temp_file = Tempfile.new(['invalid', '.xml'])
|
||||
temp_file.write('<invalid>content</invalid>')
|
||||
temp_file.rewind
|
||||
|
||||
expect(Down).to receive(:download).with(valid_url, max_size: Avatar::AvatarFromUrlJob::MAX_DOWNLOAD_SIZE)
|
||||
.and_return(ActionDispatch::Http::UploadedFile.new(tempfile: temp_file, type: 'application/xml'))
|
||||
|
||||
expect { described_class.perform_now(contact, valid_url) }.not_to raise_error
|
||||
|
||||
temp_file.close
|
||||
temp_file.unlink
|
||||
end
|
||||
|
||||
it 'skips sync attribute updates when URL is nil' do
|
||||
contact = create(:contact)
|
||||
expect(Down).not_to receive(:download)
|
||||
|
||||
expect { described_class.perform_now(contact, nil) }.not_to raise_error
|
||||
|
||||
contact.reload
|
||||
expect(contact.additional_attributes['last_avatar_sync_at']).to be_nil
|
||||
expect(contact.additional_attributes['avatar_url_hash']).to be_nil
|
||||
end
|
||||
end
|
||||
85
spec/jobs/bulk_actions_job_spec.rb
Normal file
85
spec/jobs/bulk_actions_job_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BulkActionsJob do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed' },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
subject(:job) { described_class.perform_later(account: account, params: params, user: agent) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) }
|
||||
let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) }
|
||||
let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) }
|
||||
|
||||
before do
|
||||
Conversation.all.find_each do |conversation|
|
||||
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
||||
end
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(account: account, params: params, user: agent)
|
||||
.on_queue('medium')
|
||||
end
|
||||
|
||||
context 'when job is triggered' do
|
||||
let(:bulk_action_job) { double }
|
||||
|
||||
before do
|
||||
allow(bulk_action_job).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'bulk updates the status' do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed', assignee_id: agent.id },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
expect(Conversation.first.status).to eq('open')
|
||||
|
||||
described_class.perform_now(account: account, params: params, user: agent)
|
||||
|
||||
expect(conversation_1.reload.status).to eq('snoozed')
|
||||
expect(conversation_2.reload.status).to eq('snoozed')
|
||||
expect(conversation_3.reload.status).to eq('snoozed')
|
||||
end
|
||||
|
||||
it 'bulk updates the assignee_id' do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed', assignee_id: agent.id },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
expect(Conversation.first.assignee_id).to be_nil
|
||||
|
||||
described_class.perform_now(account: account, params: params, user: agent)
|
||||
|
||||
expect(Conversation.first.assignee_id).to eq(agent.id)
|
||||
expect(Conversation.second.assignee_id).to eq(agent.id)
|
||||
expect(Conversation.third.assignee_id).to eq(agent.id)
|
||||
end
|
||||
|
||||
it 'bulk updates the snoozed_until' do
|
||||
params = {
|
||||
type: 'Conversation',
|
||||
fields: { status: 'snoozed', snoozed_until: Time.zone.now },
|
||||
ids: Conversation.first(3).pluck(:display_id)
|
||||
}
|
||||
|
||||
expect(Conversation.first.snoozed_until).to be_nil
|
||||
|
||||
described_class.perform_now(account: account, params: params, user: agent)
|
||||
|
||||
expect(Conversation.first.snoozed_until).to be_present
|
||||
expect(Conversation.second.snoozed_until).to be_present
|
||||
expect(Conversation.third.snoozed_until).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
25
spec/jobs/campaigns/trigger_oneoff_campaign_job_spec.rb
Normal file
25
spec/jobs/campaigns/trigger_oneoff_campaign_job_spec.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Campaigns::TriggerOneoffCampaignJob do
|
||||
let(:account) { create(:account) }
|
||||
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
||||
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||
let(:label1) { create(:label, account: account) }
|
||||
let(:label2) { create(:label, account: account) }
|
||||
|
||||
let!(:campaign) do
|
||||
create(:campaign, inbox: twilio_inbox, account: account, audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later(campaign) }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called with a campaign' do
|
||||
it 'triggers the campaign' do
|
||||
expect(campaign).to receive(:trigger!)
|
||||
described_class.perform_now(campaign)
|
||||
end
|
||||
end
|
||||
end
|
||||
50
spec/jobs/channels/twilio/templates_sync_job_spec.rb
Normal file
50
spec/jobs/channels/twilio/templates_sync_job_spec.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channels::Twilio::TemplatesSyncJob do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:twilio_channel) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later(twilio_channel) }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
.with(twilio_channel)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:template_sync_service) { instance_double(Twilio::TemplateSyncService) }
|
||||
|
||||
context 'with successful template sync' do
|
||||
it 'creates and calls the template sync service' do
|
||||
expect(Twilio::TemplateSyncService).to receive(:new).with(channel: twilio_channel).and_return(template_sync_service)
|
||||
expect(template_sync_service).to receive(:call).and_return(true)
|
||||
|
||||
described_class.perform_now(twilio_channel)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with template sync exception' do
|
||||
let(:error_message) { 'Twilio API error' }
|
||||
|
||||
before do
|
||||
allow(Twilio::TemplateSyncService).to receive(:new).with(channel: twilio_channel).and_return(template_sync_service)
|
||||
allow(template_sync_service).to receive(:call).and_raise(StandardError, error_message)
|
||||
end
|
||||
|
||||
it 'does not suppress the exception' do
|
||||
expect { described_class.perform_now(twilio_channel) }.to raise_error(StandardError, error_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil channel' do
|
||||
it 'handles nil channel gracefully' do
|
||||
expect { described_class.perform_now(nil) }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'job configuration' do
|
||||
it 'is configured to run on low priority queue' do
|
||||
expect(described_class.queue_name).to eq('low')
|
||||
end
|
||||
end
|
||||
end
|
||||
20
spec/jobs/channels/whatsapp/templates_sync_job_spec.rb
Normal file
20
spec/jobs/channels/whatsapp/templates_sync_job_spec.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channels::Whatsapp::TemplatesSyncJob do
|
||||
let(:channel_whatsapp) { create(:channel_whatsapp, sync_templates: false) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
expect { described_class.perform_later(channel_whatsapp) }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'calls sync_templates' do
|
||||
whatsapp_channel = double
|
||||
allow(whatsapp_channel).to receive(:sync_templates).and_return(true)
|
||||
expect(whatsapp_channel).to receive(:sync_templates)
|
||||
described_class.perform_now(whatsapp_channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channels::Whatsapp::TemplatesSyncSchedulerJob do
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'schedules templates_sync_jobs for channels where templates need to be updated' do
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
non_synced = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: nil)
|
||||
synced_recently = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: Time.zone.now)
|
||||
synced_old = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 4.hours.ago)
|
||||
described_class.perform_now
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).not_to(
|
||||
have_been_enqueued.with(synced_recently).on_queue('low')
|
||||
)
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).to(
|
||||
have_been_enqueued.with(synced_old).on_queue('low')
|
||||
)
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).to(
|
||||
have_been_enqueued.with(non_synced).on_queue('low')
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules templates_sync_job for oldest synced channels first' do
|
||||
stub_const('Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT', 2)
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
non_synced = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: nil)
|
||||
synced_recently = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 4.hours.ago)
|
||||
synced_old = create(:channel_whatsapp, sync_templates: false, message_templates_last_updated: 6.hours.ago)
|
||||
described_class.perform_now
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).not_to(
|
||||
have_been_enqueued.with(synced_recently).on_queue('low')
|
||||
)
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).to(
|
||||
have_been_enqueued.with(synced_old).on_queue('low')
|
||||
)
|
||||
expect(Channels::Whatsapp::TemplatesSyncJob).to(
|
||||
have_been_enqueued.with(non_synced).on_queue('low')
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
22
spec/jobs/contacts/bulk_action_job_spec.rb
Normal file
22
spec/jobs/contacts/bulk_action_job_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Contacts::BulkActionJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:params) { { 'ids' => [1], 'labels' => { 'add' => ['vip'] } } }
|
||||
|
||||
it 'invokes the bulk action service with account and user' do
|
||||
service_instance = instance_double(Contacts::BulkActionService, perform: true)
|
||||
|
||||
allow(Contacts::BulkActionService).to receive(:new).and_return(service_instance)
|
||||
|
||||
described_class.perform_now(account.id, user.id, params)
|
||||
|
||||
expect(Contacts::BulkActionService).to have_received(:new).with(
|
||||
account: account,
|
||||
user: user,
|
||||
params: params
|
||||
)
|
||||
expect(service_instance).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
33
spec/jobs/conversation_reply_email_job_spec.rb
Normal file
33
spec/jobs/conversation_reply_email_job_spec.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ConversationReplyEmailJob, type: :job do
|
||||
let(:conversation) { create(:conversation) }
|
||||
let(:mailer) { double }
|
||||
let(:mailer_action) { double }
|
||||
|
||||
before do
|
||||
allow(Conversation).to receive(:find).and_return(conversation)
|
||||
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||
allow(mailer).to receive(:reply_with_summary).and_return(mailer_action)
|
||||
allow(mailer).to receive(:reply_without_summary).and_return(mailer_action)
|
||||
allow(mailer_action).to receive(:deliver_later).and_return(true)
|
||||
end
|
||||
|
||||
it 'enqueues on mailers queue' do
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
expect do
|
||||
described_class.perform_later(conversation.id, 123)
|
||||
end.to have_enqueued_job(described_class).on_queue('mailers')
|
||||
end
|
||||
|
||||
it 'calls reply_with_summary when last incoming message was not email' do
|
||||
described_class.perform_now(conversation.id, 123)
|
||||
expect(mailer).to have_received(:reply_with_summary)
|
||||
end
|
||||
|
||||
it 'calls reply_without_summary when last incoming message was email' do
|
||||
create(:message, conversation: conversation, message_type: :incoming, content_type: 'incoming_email')
|
||||
described_class.perform_now(conversation.id, 123)
|
||||
expect(mailer).to have_received(:reply_without_summary)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Conversations::ReopenSnoozedConversationsJob do
|
||||
let!(:snoozed_till_5_minutes_ago) { create(:conversation, status: :snoozed, snoozed_until: 5.minutes.ago) }
|
||||
let!(:snoozed_till_tomorrow) { create(:conversation, status: :snoozed, snoozed_until: 1.day.from_now) }
|
||||
let!(:snoozed_indefinitely) { create(:conversation, status: :snoozed) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'reopens snoozed conversations whose snooze until has passed' do
|
||||
described_class.perform_now
|
||||
|
||||
expect(snoozed_till_5_minutes_ago.reload.status).to eq 'open'
|
||||
expect(snoozed_till_tomorrow.reload.status).to eq 'snoozed'
|
||||
expect(snoozed_indefinitely.reload.status).to eq 'snoozed'
|
||||
end
|
||||
end
|
||||
end
|
||||
83
spec/jobs/conversations/resolution_job_spec.rb
Normal file
83
spec/jobs/conversations/resolution_job_spec.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Conversations::ResolutionJob do
|
||||
subject(:job) { described_class.perform_later(account: account) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let(:label) { create(:label, title: 'auto-resolved', account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(account: account)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'does nothing when there is no auto resolve duration' do
|
||||
described_class.perform_now(account: account)
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
|
||||
context 'when auto_resolve_ignore_waiting is true' do
|
||||
it 'resolves non-waiting conversations if time of inactivity is more than auto resolve duration' do
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: true) # 10 days in minutes
|
||||
conversation.update(last_activity_at: 13.days.ago, waiting_since: nil)
|
||||
described_class.perform_now(account: account)
|
||||
expect(conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
|
||||
it 'does not resolve waiting conversations even if time of inactivity is more than auto resolve duration' do
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: true) # 10 days in minutes
|
||||
conversation.update(last_activity_at: 13.days.ago, waiting_since: 13.days.ago)
|
||||
described_class.perform_now(account: account)
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when auto_resolve_ignore_waiting is false' do
|
||||
it 'resolves all conversations if time of inactivity is more than auto resolve duration' do
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes
|
||||
# Create one waiting conversation and one non-waiting conversation
|
||||
waiting_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: 13.days.ago)
|
||||
non_waiting_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: nil)
|
||||
|
||||
described_class.perform_now(account: account)
|
||||
|
||||
expect(waiting_conversation.reload.status).to eq('resolved')
|
||||
expect(non_waiting_conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
end
|
||||
|
||||
# When a contact is deleted, there's a brief window (~50-150ms) where contact_id becomes nil
|
||||
# but conversations still exist. If ResolutionJob runs during this window, muted? can crash
|
||||
# trying to call blocked? on nil. Fixes # (issue).
|
||||
it 'skips orphan conversations without a contact' do
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes
|
||||
orphan_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: nil)
|
||||
orphan_conversation.update_columns(contact_id: nil, contact_inbox_id: nil) # rubocop:disable Rails/SkipsModelValidations
|
||||
resolvable_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: nil)
|
||||
|
||||
described_class.perform_now(account: account)
|
||||
|
||||
expect(orphan_conversation.reload.status).to eq('open')
|
||||
expect(resolvable_conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
|
||||
it 'adds a label after resolution' do
|
||||
account.update(auto_resolve_label: 'auto-resolved', auto_resolve_after: 14_400)
|
||||
conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: 13.days.ago)
|
||||
|
||||
described_class.perform_now(account: account)
|
||||
|
||||
expect(conversation.reload.status).to eq('resolved')
|
||||
expect(conversation.reload.label_list).to include('auto-resolved')
|
||||
end
|
||||
|
||||
it 'resolves only a limited number of conversations in a single execution' do
|
||||
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes
|
||||
create_list(:conversation, 3, account: account, last_activity_at: 13.days.ago)
|
||||
described_class.perform_now(account: account)
|
||||
expect(account.conversations.resolved.count).to eq(Limits::BULK_ACTIONS_LIMIT)
|
||||
end
|
||||
end
|
||||
80
spec/jobs/conversations/update_message_status_job_spec.rb
Normal file
80
spec/jobs/conversations/update_message_status_job_spec.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Conversations::UpdateMessageStatusJob do
|
||||
subject(:job) { described_class.perform_later(conversation.id, conversation.contact_last_seen_at, :read) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:conversation) { create(:conversation, account: account, contact_last_seen_at: DateTime.now.utc) }
|
||||
let!(:message) { create(:message, conversation: conversation, message_type: 'outgoing', status: 'sent', created_at: 1.day.ago) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(conversation.id, conversation.contact_last_seen_at, :read)
|
||||
.on_queue('deferred')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'marks all sent messages in a conversation as read' do
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
message.reload
|
||||
end.to change(message, :status).from('sent').to('read')
|
||||
end
|
||||
|
||||
it 'marks all sent messages in a conversation as delivered if specified' do
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at, :delivered)
|
||||
message.reload
|
||||
end.to change(message, :status).from('sent').to('delivered')
|
||||
end
|
||||
|
||||
it 'marks all delivered messages in a conversation as read' do
|
||||
message.update!(status: 'delivered')
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
message.reload
|
||||
end.to change(message, :status).from('delivered').to('read')
|
||||
end
|
||||
|
||||
it 'marks all templates messages in a conversation as read' do
|
||||
message.update!(status: 'delivered', message_type: 'template')
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
message.reload
|
||||
end.to change(message, :status).from('delivered').to('read')
|
||||
end
|
||||
|
||||
it 'does not mark failed messages as read' do
|
||||
message.update!(status: 'failed')
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
end.not_to change(message.reload, :status)
|
||||
end
|
||||
|
||||
it 'does not mark incoming messages as read' do
|
||||
message.update!(message_type: 'incoming')
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
end.not_to change(message.reload, :status)
|
||||
end
|
||||
|
||||
it 'does not mark messages created after the contact last seen time as read' do
|
||||
message.update!(created_at: DateTime.now.utc)
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at)
|
||||
end.not_to change(message.reload, :status)
|
||||
end
|
||||
|
||||
it 'does not run the job if the conversation does not exist' do
|
||||
expect do
|
||||
described_class.perform_now(1212, conversation.contact_last_seen_at)
|
||||
end.not_to change(message.reload, :status)
|
||||
end
|
||||
|
||||
it 'does not run the job if the status is failed' do
|
||||
expect do
|
||||
described_class.perform_now(conversation.id, conversation.contact_last_seen_at, :failed)
|
||||
end.not_to change(message.reload, :status)
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/jobs/crm/setup_job_spec.rb
Normal file
85
spec/jobs/crm/setup_job_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Crm::SetupJob do
|
||||
subject(:job) { described_class.perform_later(hook.id) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) do
|
||||
create(:integrations_hook,
|
||||
account: account,
|
||||
app_id: 'leadsquared',
|
||||
settings: {
|
||||
access_key: 'test_key',
|
||||
secret_key: 'test_token',
|
||||
endpoint_url: 'https://api.leadsquared.com'
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
account.enable_features('crm_integration')
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(hook.id)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when hook is not found' do
|
||||
it 'returns without processing' do
|
||||
allow(Integrations::Hook).to receive(:find_by).and_return(nil)
|
||||
expect(described_class.new.perform(0)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is disabled' do
|
||||
it 'returns without processing' do
|
||||
disabled_hook = create(:integrations_hook,
|
||||
account: account,
|
||||
app_id: 'leadsquared',
|
||||
status: 'disabled',
|
||||
settings: {
|
||||
access_key: 'test_key',
|
||||
secret_key: 'test_token',
|
||||
endpoint_url: 'https://api.leadsquared.com'
|
||||
})
|
||||
expect(described_class.new.perform(disabled_hook.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is not a CRM integration' do
|
||||
it 'returns without processing' do
|
||||
non_crm_hook = create(:integrations_hook,
|
||||
account: account,
|
||||
app_id: 'slack',
|
||||
settings: { webhook_url: 'https://slack.com/webhook' })
|
||||
expect(described_class.new.perform(non_crm_hook.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is valid' do
|
||||
let(:setup_service) { instance_double(Crm::Leadsquared::SetupService) }
|
||||
|
||||
before do
|
||||
allow(Crm::Leadsquared::SetupService).to receive(:new).with(hook).and_return(setup_service)
|
||||
end
|
||||
|
||||
context 'when setup raises an error' do
|
||||
it 'captures exception and logs error' do
|
||||
error = StandardError.new('Test error')
|
||||
allow(setup_service).to receive(:setup).and_raise(error)
|
||||
allow(Rails.logger).to receive(:error)
|
||||
allow(ChatwootExceptionTracker).to receive(:new)
|
||||
.with(error, account: hook.account)
|
||||
.and_return(instance_double(ChatwootExceptionTracker, capture_exception: true))
|
||||
|
||||
described_class.new.perform(hook.id)
|
||||
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
.with("Error in CRM setup for hook ##{hook.id} (#{hook.app_id}): Test error")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
175
spec/jobs/data_import_job_spec.rb
Normal file
175
spec/jobs/data_import_job_spec.rb
Normal file
@@ -0,0 +1,175 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DataImportJob do
|
||||
subject(:job) { described_class.perform_later(data_import) }
|
||||
|
||||
let!(:data_import) { create(:data_import) }
|
||||
|
||||
describe 'enqueueing the job' do
|
||||
it 'queues the job on the low priority queue' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(data_import)
|
||||
.on_queue('low')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'retrying the job' do
|
||||
context 'when ActiveStorage::FileNotFoundError is raised' do
|
||||
let(:import_file_double) { instance_double(ActiveStorage::Blob) }
|
||||
|
||||
before do
|
||||
allow(data_import).to receive(:import_file).and_return(import_file_double)
|
||||
allow(import_file_double).to receive(:open).and_raise(ActiveStorage::FileNotFoundError)
|
||||
end
|
||||
|
||||
it 'retries the job' do
|
||||
expect do
|
||||
described_class.perform_now(data_import)
|
||||
end.to have_enqueued_job(described_class).at_least(1).times
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'importing data' do
|
||||
context 'when the data is valid' do
|
||||
it 'imports data into the account' do
|
||||
csv_length = CSV.parse(data_import.import_file.download, headers: true).length
|
||||
|
||||
described_class.perform_now(data_import)
|
||||
expect(data_import.account.contacts.count).to eq(csv_length)
|
||||
expect(data_import.reload.total_records).to eq(csv_length)
|
||||
expect(data_import.reload.processed_records).to eq(csv_length)
|
||||
contact = Contact.find_by(phone_number: '+918080808080')
|
||||
expect(contact).to be_truthy
|
||||
expect(contact['additional_attributes']['company']).to eq('My Company Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the data contains errors' do
|
||||
it 'imports erroneous data into the account, skipping invalid records' do
|
||||
# Last record is invalid because of duplicate email
|
||||
invalid_data = [
|
||||
%w[id first_name last_name email phone_number],
|
||||
['1', 'Clarice', 'Uzzell', 'cuzzell0@mozilla.org', '+918484848484'],
|
||||
['2', 'Marieann', 'Creegan', 'mcreegan1@cornell.edu', '+918484848485'],
|
||||
['3', 'Nancey', 'Windibank', 'cuzzell0@mozilla.org', '+91848484848']
|
||||
]
|
||||
|
||||
invalid_data_import = create(:data_import, import_file: generate_csv_file(invalid_data))
|
||||
csv_data = CSV.parse(invalid_data_import.import_file.download, headers: true)
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(invalid_data_import)
|
||||
expect(invalid_data_import.account.contacts.count).to eq(csv_length - 1)
|
||||
expect(invalid_data_import.reload.total_records).to eq(csv_length)
|
||||
expect(invalid_data_import.reload.processed_records).to eq(csv_length)
|
||||
end
|
||||
|
||||
it 'will preserve emojis' do
|
||||
data_import = create(:data_import,
|
||||
import_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/data_import/with_emoji.csv'),
|
||||
'text/csv'))
|
||||
csv_data = CSV.parse(data_import.import_file.download, headers: true)
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(data_import)
|
||||
expect(data_import.account.contacts.count).to eq(csv_length)
|
||||
|
||||
expect(data_import.account.contacts.first.name).to eq('T 🏠 🔥 Test')
|
||||
end
|
||||
|
||||
it 'will not throw error for non utf-8 characters' do
|
||||
invalid_data_import = create(:data_import,
|
||||
import_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/data_import/invalid_bytes.csv'),
|
||||
'text/csv'))
|
||||
csv_data = CSV.parse(invalid_data_import.import_file.download, headers: true)
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(invalid_data_import)
|
||||
expect(invalid_data_import.account.contacts.count).to eq(csv_length)
|
||||
|
||||
expect(invalid_data_import.account.contacts.first.name).to eq(csv_data[0]['name'].encode('UTF-8', 'binary', invalid: :replace,
|
||||
undef: :replace, replace: ''))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the data contains existing records' do
|
||||
let(:existing_data) do
|
||||
[
|
||||
%w[id name email phone_number company],
|
||||
['1', 'Clarice Uzzell', 'cuzzell0@mozilla.org', '918080808080', 'Acmecorp'],
|
||||
['2', 'Marieann Creegan', 'mcreegan1@cornell.edu', '+918080808081', 'Acmecorp'],
|
||||
['3', 'Nancey Windibank', 'nwindibank2@bluehost.com', '+918080808082', 'Acmecorp']
|
||||
]
|
||||
end
|
||||
let(:existing_data_import) { create(:data_import, import_file: generate_csv_file(existing_data)) }
|
||||
let(:csv_data) { CSV.parse(existing_data_import.import_file.download, headers: true) }
|
||||
|
||||
context 'when the existing record has an email in import data' do
|
||||
it 'updates the existing record with new data' do
|
||||
contact = Contact.create!(email: csv_data[0]['email'], account_id: existing_data_import.account_id)
|
||||
expect(contact.reload.phone_number).to be_nil
|
||||
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(existing_data_import)
|
||||
expect(existing_data_import.account.contacts.count).to eq(csv_length)
|
||||
contact = Contact.from_email(csv_data[0]['email'])
|
||||
expect(contact).to be_present
|
||||
expect(contact.phone_number).to eq("+#{csv_data[0]['phone_number']}")
|
||||
expect(contact.name).to eq((csv_data[0]['name']).to_s)
|
||||
expect(contact.additional_attributes['company']).to eq((csv_data[0]['company']).to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the existing record has a phone_number in import data' do
|
||||
it 'updates the existing record with new data' do
|
||||
contact = Contact.create!(account_id: existing_data_import.account_id, phone_number: csv_data[1]['phone_number'])
|
||||
expect(contact.reload.email).to be_nil
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(existing_data_import)
|
||||
expect(existing_data_import.account.contacts.count).to eq(csv_length)
|
||||
|
||||
contact = Contact.find_by(phone_number: "+#{csv_data[0]['phone_number']}")
|
||||
expect(contact).to be_present
|
||||
expect(contact.email).to eq(csv_data[0]['email'])
|
||||
expect(contact.name).to eq((csv_data[0]['name']).to_s)
|
||||
expect(contact.additional_attributes['company']).to eq((csv_data[0]['company']).to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the existing record has both email and phone_number in import data' do
|
||||
it 'skips importing the records' do
|
||||
phone_contact = Contact.create!(account_id: existing_data_import.account_id, phone_number: csv_data[1]['phone_number'])
|
||||
email_contact = Contact.create!(account_id: existing_data_import.account_id, email: csv_data[1]['email'])
|
||||
|
||||
csv_length = csv_data.length
|
||||
|
||||
described_class.perform_now(existing_data_import)
|
||||
expect(phone_contact.reload.email).to be_nil
|
||||
expect(email_contact.reload.phone_number).to be_nil
|
||||
expect(existing_data_import.total_records).to eq(csv_length)
|
||||
expect(existing_data_import.processed_records).to eq(csv_length - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the CSV file is invalid' do
|
||||
let(:invalid_csv_content) do
|
||||
"id,name,email,phone_number,company\n1,\"Clarice Uzzell,\"missing_quote,918080808080,Acmecorp\n2,Marieann Creegan,,+918080808081,Acmecorp"
|
||||
end
|
||||
|
||||
before do
|
||||
import_file_double = instance_double(ActiveStorage::Blob)
|
||||
allow(data_import).to receive(:import_file).and_return(import_file_double)
|
||||
allow(import_file_double).to receive(:open).and_yield(StringIO.new(invalid_csv_content))
|
||||
end
|
||||
|
||||
it 'does not import any data and handles the MalformedCSVError' do
|
||||
expect { described_class.perform_now(data_import) }
|
||||
.to change { data_import.reload.status }.from('pending').to('failed')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
76
spec/jobs/delete_object_job_spec.rb
Normal file
76
spec/jobs/delete_object_job_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DeleteObjectJob, type: :job do
|
||||
describe '#perform' do
|
||||
context 'when object is heavy (Inbox)' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create_list(:conversation, 3, account: account, inbox: inbox)
|
||||
ReportingEvent.create!(account: account, inbox: inbox, name: 'inbox_metric', value: 1.0)
|
||||
end
|
||||
|
||||
it 'enqueues on the low queue' do
|
||||
expect { described_class.perform_later(inbox) }
|
||||
.to have_enqueued_job(described_class).with(inbox).on_queue('low')
|
||||
end
|
||||
|
||||
it 'pre-deletes heavy associations and then destroys the object' do
|
||||
conv_ids = inbox.conversations.pluck(:id)
|
||||
ci_ids = inbox.contact_inboxes.pluck(:id)
|
||||
contact_ids = inbox.contacts.pluck(:id)
|
||||
re_ids = inbox.reporting_events.pluck(:id)
|
||||
|
||||
described_class.perform_now(inbox)
|
||||
|
||||
# Reload associations to ensure database state is current
|
||||
expect(Conversation.where(id: conv_ids).reload).to be_empty
|
||||
expect(ContactInbox.where(id: ci_ids).reload).to be_empty
|
||||
expect(ReportingEvent.where(id: re_ids).reload).to be_empty
|
||||
# Contacts should not be deleted for inbox destroy
|
||||
expect(Contact.where(id: contact_ids).reload).not_to be_empty
|
||||
expect { inbox.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when object is heavy (Account)' do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox1) { create(:inbox, account: account) }
|
||||
let!(:inbox2) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create_list(:conversation, 2, account: account, inbox: inbox1)
|
||||
create_list(:conversation, 1, account: account, inbox: inbox2)
|
||||
ReportingEvent.create!(account: account, name: 'acct_metric', value: 2.5)
|
||||
ReportingEvent.create!(account: account, inbox: inbox1, name: 'acct_inbox_metric', value: 3.5)
|
||||
end
|
||||
|
||||
it 'pre-deletes conversations, contacts, inboxes and reporting events and then destroys the account' do
|
||||
conv_ids = account.conversations.pluck(:id)
|
||||
contact_ids = account.contacts.pluck(:id)
|
||||
inbox_ids = account.inboxes.pluck(:id)
|
||||
re_ids = account.reporting_events.pluck(:id)
|
||||
|
||||
described_class.perform_now(account)
|
||||
|
||||
# Reload associations to ensure database state is current
|
||||
expect(Conversation.where(id: conv_ids).reload).to be_empty
|
||||
expect(Contact.where(id: contact_ids).reload).to be_empty
|
||||
expect(Inbox.where(id: inbox_ids).reload).to be_empty
|
||||
expect(ReportingEvent.where(id: re_ids).reload).to be_empty
|
||||
expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when object is regular (Label)' do
|
||||
it 'just destroys the object' do
|
||||
label = create(:label)
|
||||
|
||||
described_class.perform_now(label)
|
||||
|
||||
expect { label.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
spec/jobs/event_dispatcher_job_spec.rb
Normal file
22
spec/jobs/event_dispatcher_job_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe EventDispatcherJob do
|
||||
subject(:job) { described_class.perform_later(event_name, timestamp, event_data) }
|
||||
|
||||
let!(:conversation) { create(:conversation) }
|
||||
let(:event_name) { 'conversation.created' }
|
||||
let(:timestamp) { Time.zone.now }
|
||||
let(:event_data) { { conversation: conversation } }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(event_name, timestamp, event_data)
|
||||
.on_queue('critical')
|
||||
end
|
||||
|
||||
it 'publishes event' do
|
||||
expect(Rails.configuration.dispatcher.async_dispatcher).to receive(:publish_event).with(event_name, timestamp, event_data).once
|
||||
event_dispatcher = described_class.new
|
||||
event_dispatcher.perform(event_name, timestamp, event_data)
|
||||
end
|
||||
end
|
||||
175
spec/jobs/hook_job_spec.rb
Normal file
175
spec/jobs/hook_job_spec.rb
Normal file
@@ -0,0 +1,175 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe HookJob do
|
||||
subject(:job) { described_class.perform_later(hook, event_name, event_data) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:event_name) { 'message.created' }
|
||||
let(:event_data) { { message: create(:message, account: account, content: 'muchas muchas gracias', message_type: :incoming) } }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(hook, event_name, event_data)
|
||||
.on_queue('medium')
|
||||
end
|
||||
|
||||
context 'when the hook is disabled' do
|
||||
it 'does not execute the job' do
|
||||
hook = create(:integrations_hook, status: 'disabled', account: account)
|
||||
allow(SendOnSlackJob).to receive(:perform_later)
|
||||
allow(Integrations::Dialogflow::ProcessorService).to receive(:new)
|
||||
allow(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new)
|
||||
|
||||
expect(SendOnSlackJob).not_to receive(:perform_later)
|
||||
expect(Integrations::GoogleTranslate::DetectLanguageService).not_to receive(:new)
|
||||
expect(Integrations::Dialogflow::ProcessorService).not_to receive(:new)
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handleable events like message.created' do
|
||||
let(:process_service) { double }
|
||||
|
||||
before do
|
||||
allow(process_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'calls SendOnSlackJob when its a slack hook' do
|
||||
hook = create(:integrations_hook, app_id: 'slack', account: account)
|
||||
allow(SendOnSlackJob).to receive(:perform_later).and_return(process_service)
|
||||
expect(SendOnSlackJob).to receive(:perform_later).with(event_data[:message], hook)
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
|
||||
it 'calls SendOnSlackJob when its a slack hook for message with attachments' do
|
||||
event_data = { message: create(:message, :with_attachment, account: account) }
|
||||
hook = create(:integrations_hook, app_id: 'slack', account: account)
|
||||
allow(SendOnSlackJob).to receive(:set).with(wait: 2.seconds).and_return(SendOnSlackJob)
|
||||
allow(SendOnSlackJob).to receive(:perform_later).and_return(process_service)
|
||||
expect(SendOnSlackJob).to receive(:perform_later).with(event_data[:message], hook)
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
|
||||
it 'calls Integrations::Dialogflow::ProcessorService when its a dialogflow intergation' do
|
||||
hook = create(:integrations_hook, :dialogflow, inbox: inbox, account: account)
|
||||
allow(Integrations::Dialogflow::ProcessorService).to receive(:new).and_return(process_service)
|
||||
expect(Integrations::Dialogflow::ProcessorService).to receive(:new).with(event_name: event_name, hook: hook, event_data: event_data)
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
|
||||
it 'calls Conversations::DetectLanguageJob when its a google_translate intergation' do
|
||||
hook = create(:integrations_hook, :google_translate, account: account)
|
||||
allow(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new).and_return(process_service)
|
||||
expect(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new).with(hook: hook, message: event_data[:message])
|
||||
described_class.perform_now(hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing leadsquared integration' do
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, contact: contact) }
|
||||
let(:processor_service) { instance_double(Crm::Leadsquared::ProcessorService) }
|
||||
let(:leadsquared_hook) { instance_double(Integrations::Hook, id: 123, app_id: 'leadsquared', account: account) }
|
||||
|
||||
before do
|
||||
allow(Crm::Leadsquared::ProcessorService).to receive(:new).with(leadsquared_hook).and_return(processor_service)
|
||||
end
|
||||
|
||||
context 'when processing contact.updated event' do
|
||||
let(:event_name) { 'contact.updated' }
|
||||
let(:event_data) { { contact: contact } }
|
||||
|
||||
it 'uses a lock when processing' do
|
||||
allow(leadsquared_hook).to receive(:disabled?).and_return(false)
|
||||
allow(leadsquared_hook).to receive(:feature_allowed?).and_return(true)
|
||||
allow(processor_service).to receive(:handle_contact).with(contact)
|
||||
|
||||
# Mock the with_lock method directly on the job instance
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:with_lock).and_yield
|
||||
allow(described_class).to receive(:new).and_return(job_instance)
|
||||
|
||||
expect(job_instance).to receive(:with_lock).with(
|
||||
format(Redis::Alfred::CRM_PROCESS_MUTEX, hook_id: leadsquared_hook.id)
|
||||
)
|
||||
|
||||
job_instance.perform(leadsquared_hook, event_name, event_data)
|
||||
end
|
||||
|
||||
it 'does not process when feature is not allowed' do
|
||||
allow(leadsquared_hook).to receive(:disabled?).and_return(false)
|
||||
allow(leadsquared_hook).to receive(:feature_allowed?).and_return(false)
|
||||
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:with_lock)
|
||||
|
||||
expect(job_instance).not_to receive(:with_lock)
|
||||
expect(processor_service).not_to receive(:handle_contact)
|
||||
|
||||
job_instance.perform(leadsquared_hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing conversation.created event' do
|
||||
let(:event_name) { 'conversation.created' }
|
||||
let(:event_data) { { conversation: conversation } }
|
||||
|
||||
it 'uses a lock when processing' do
|
||||
allow(leadsquared_hook).to receive(:disabled?).and_return(false)
|
||||
allow(leadsquared_hook).to receive(:feature_allowed?).and_return(true)
|
||||
allow(processor_service).to receive(:handle_conversation_created).with(conversation)
|
||||
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:with_lock).and_yield
|
||||
allow(described_class).to receive(:new).and_return(job_instance)
|
||||
|
||||
expect(job_instance).to receive(:with_lock).with(
|
||||
format(Redis::Alfred::CRM_PROCESS_MUTEX, hook_id: leadsquared_hook.id)
|
||||
)
|
||||
|
||||
job_instance.perform(leadsquared_hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing conversation.resolved event' do
|
||||
let(:event_name) { 'conversation.resolved' }
|
||||
let(:event_data) { { conversation: conversation } }
|
||||
|
||||
it 'uses a lock when processing' do
|
||||
allow(leadsquared_hook).to receive(:disabled?).and_return(false)
|
||||
allow(leadsquared_hook).to receive(:feature_allowed?).and_return(true)
|
||||
allow(processor_service).to receive(:handle_conversation_resolved).with(conversation)
|
||||
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:with_lock).and_yield
|
||||
allow(described_class).to receive(:new).and_return(job_instance)
|
||||
|
||||
expect(job_instance).to receive(:with_lock).with(
|
||||
format(Redis::Alfred::CRM_PROCESS_MUTEX, hook_id: leadsquared_hook.id)
|
||||
)
|
||||
|
||||
job_instance.perform(leadsquared_hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing invalid event' do
|
||||
let(:event_name) { 'invalid.event' }
|
||||
let(:event_data) { { contact: contact } }
|
||||
|
||||
it 'does not process for invalid event names' do
|
||||
allow(leadsquared_hook).to receive(:disabled?).and_return(false)
|
||||
allow(leadsquared_hook).to receive(:feature_allowed?).and_return(true)
|
||||
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:with_lock)
|
||||
|
||||
expect(job_instance).not_to receive(:with_lock)
|
||||
expect(processor_service).not_to receive(:handle_contact)
|
||||
|
||||
job_instance.perform(leadsquared_hook, event_name, event_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
93
spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb
Normal file
93
spec/jobs/inboxes/bulk_auto_assignment_job_spec.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::BulkAutoAssignmentJob do
|
||||
let(:account) { create(:account, custom_attributes: { 'plan_name' => 'Startups' }) }
|
||||
let(:agent) { create(:user, account: account, role: :agent, auto_offline: false) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: nil, status: :open) }
|
||||
let(:assignment_service) { double }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
allow(assignment_service).to receive(:perform)
|
||||
end
|
||||
|
||||
context 'when inbox has inbox members' do
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: inbox)
|
||||
account.enable_features!('assignment_v2')
|
||||
inbox.update!(enable_auto_assignment: true)
|
||||
end
|
||||
|
||||
it 'assigns unassigned conversations in enabled inboxes' do
|
||||
allow(AutoAssignment::AgentAssignmentService).to receive(:new).with(
|
||||
conversation: conversation,
|
||||
allowed_agent_ids: [agent.id]
|
||||
).and_return(assignment_service)
|
||||
|
||||
described_class.perform_now
|
||||
expect(AutoAssignment::AgentAssignmentService).to have_received(:new).with(
|
||||
conversation: conversation,
|
||||
allowed_agent_ids: [agent.id]
|
||||
)
|
||||
end
|
||||
|
||||
it 'skips inboxes with auto assignment disabled' do
|
||||
inbox.update!(enable_auto_assignment: false)
|
||||
allow(AutoAssignment::AgentAssignmentService).to receive(:new)
|
||||
|
||||
described_class.perform_now
|
||||
|
||||
expect(AutoAssignment::AgentAssignmentService).not_to have_received(:new).with(
|
||||
conversation: conversation,
|
||||
allowed_agent_ids: [agent.id]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when account is on default plan in chatwoot cloud' do
|
||||
before do
|
||||
account.update!(custom_attributes: {})
|
||||
InstallationConfig.create(name: 'CHATWOOT_CLOUD_PLANS', value: [{ 'name' => 'default' }])
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
end
|
||||
|
||||
it 'skips auto assignment' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
expect(Rails.logger).to receive(:info).with("Skipping auto assignment for account #{account.id}")
|
||||
|
||||
allow(AutoAssignment::AgentAssignmentService).to receive(:new)
|
||||
expect(AutoAssignment::AgentAssignmentService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox has no members' do
|
||||
before do
|
||||
account.enable_features!('assignment_v2')
|
||||
inbox.update!(enable_auto_assignment: true)
|
||||
end
|
||||
|
||||
it 'does not assign conversations' do
|
||||
allow(Rails.logger).to receive(:info)
|
||||
expect(Rails.logger).to receive(:info).with("No agents available to assign conversation to inbox #{inbox.id}")
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assignment_v2 feature is disabled' do
|
||||
before do
|
||||
account.disable_features!('assignment_v2')
|
||||
end
|
||||
|
||||
it 'skips auto assignment' do
|
||||
allow(AutoAssignment::AgentAssignmentService).to receive(:new)
|
||||
expect(AutoAssignment::AgentAssignmentService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
68
spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
Normal file
68
spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::FetchImapEmailInboxesJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:suspended_account) { create(:account, status: 'suspended') }
|
||||
let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) }
|
||||
|
||||
let(:imap_email_channel) do
|
||||
create(:channel_email, imap_enabled: true, account: account)
|
||||
end
|
||||
|
||||
let(:imap_email_channel_suspended) do
|
||||
create(:channel_email, imap_enabled: true, account: suspended_account)
|
||||
end
|
||||
|
||||
let(:disabled_imap_channel) do
|
||||
create(:channel_email, imap_enabled: false, account: account)
|
||||
end
|
||||
|
||||
let(:reauth_required_channel) do
|
||||
create(:channel_email, imap_enabled: true, account: account)
|
||||
end
|
||||
|
||||
let(:premium_imap_channel) do
|
||||
create(:channel_email, imap_enabled: true, account: premium_account)
|
||||
end
|
||||
|
||||
before do
|
||||
reauth_required_channel.prompt_reauthorization!
|
||||
premium_account.custom_attributes['plan_name'] = 'Startups'
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'fetches emails only for active accounts with imap enabled' do
|
||||
# Should call perform_later only once for the active, imap-enabled inbox
|
||||
expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(imap_email_channel).once
|
||||
|
||||
# Should not call for suspended account or disabled IMAP channels
|
||||
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
|
||||
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'skips suspended accounts' do
|
||||
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'skips disabled imap channels' do
|
||||
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'skips channels requiring reauthorization' do
|
||||
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(reauth_required_channel)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
123
spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
Normal file
123
spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
Normal file
@@ -0,0 +1,123 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::FetchImapEmailsJob do
|
||||
include ActiveJob::TestHelper
|
||||
include ActionMailbox::TestHelper
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:imap_email_channel) { create(:channel_email, :imap_email, account: account) }
|
||||
let(:channel_with_imap_disabled) { create(:channel_email, :imap_email, imap_enabled: false, account: account) }
|
||||
let(:microsoft_imap_email_channel) { create(:channel_email, :microsoft_email) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'enqueues the job' do
|
||||
expect do
|
||||
described_class.perform_later(imap_email_channel, 1)
|
||||
end.to have_enqueued_job(described_class).on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
context 'when IMAP is disabled' do
|
||||
it 'does not fetch emails' do
|
||||
expect(Imap::FetchEmailService).not_to receive(:new)
|
||||
expect(Imap::MicrosoftFetchEmailService).not_to receive(:new)
|
||||
described_class.perform_now(channel_with_imap_disabled)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when IMAP reauthorization is required' do
|
||||
it 'does not fetch emails' do
|
||||
10.times do
|
||||
imap_email_channel.authorization_error!
|
||||
end
|
||||
|
||||
expect(Imap::FetchEmailService).not_to receive(:new)
|
||||
# Confirm the imap_enabled flag is true to avoid false positives.
|
||||
expect(imap_email_channel.imap_enabled?).to be true
|
||||
|
||||
described_class.perform_now(imap_email_channel)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the channel is regular imap' do
|
||||
it 'calls the imap fetch service' do
|
||||
fetch_service = double
|
||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||
allow(fetch_service).to receive(:perform).and_return([])
|
||||
|
||||
described_class.perform_now(imap_email_channel)
|
||||
expect(fetch_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'calls the imap fetch service with the correct interval' do
|
||||
fetch_service = double
|
||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 4).and_return(fetch_service)
|
||||
allow(fetch_service).to receive(:perform).and_return([])
|
||||
|
||||
described_class.perform_now(imap_email_channel, 4)
|
||||
expect(fetch_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the channel is Microsoft' do
|
||||
it 'calls the Microsoft fetch service' do
|
||||
fetch_service = double
|
||||
allow(Imap::MicrosoftFetchEmailService).to receive(:new).with(channel: microsoft_imap_email_channel, interval: 1).and_return(fetch_service)
|
||||
allow(fetch_service).to receive(:perform).and_return([])
|
||||
|
||||
described_class.perform_now(microsoft_imap_email_channel)
|
||||
expect(fetch_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when IMAP OAuth errors out' do
|
||||
it 'marks the connection as requiring authorization' do
|
||||
error_response = double
|
||||
oauth_error = OAuth2::Error.new(error_response)
|
||||
|
||||
allow(Imap::MicrosoftFetchEmailService).to receive(:new)
|
||||
.with(channel: microsoft_imap_email_channel, interval: 1)
|
||||
.and_raise(oauth_error)
|
||||
|
||||
allow(Redis::Alfred).to receive(:incr)
|
||||
|
||||
expect(Redis::Alfred).to receive(:incr)
|
||||
.with("AUTHORIZATION_ERROR_COUNT:channel_email:#{microsoft_imap_email_channel.id}")
|
||||
|
||||
described_class.perform_now(microsoft_imap_email_channel)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the fetch service returns the email objects' do
|
||||
let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail }
|
||||
let(:mailbox) { double }
|
||||
let(:exception_tracker) { double }
|
||||
let(:fetch_service) { double }
|
||||
|
||||
before do
|
||||
allow(Imap::ImapMailbox).to receive(:new).and_return(mailbox)
|
||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||
|
||||
allow(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
|
||||
end
|
||||
|
||||
it 'calls the mailbox to create emails' do
|
||||
allow(mailbox).to receive(:process)
|
||||
|
||||
expect(Imap::FetchEmailService).to receive(:new).with(channel: imap_email_channel, interval: 1).and_return(fetch_service)
|
||||
expect(fetch_service).to receive(:perform).and_return([inbound_mail])
|
||||
expect(mailbox).to receive(:process).with(inbound_mail, imap_email_channel)
|
||||
|
||||
described_class.perform_now(imap_email_channel)
|
||||
end
|
||||
|
||||
it 'logs errors if mailbox returns errors' do
|
||||
allow(mailbox).to receive(:process).and_raise(StandardError)
|
||||
|
||||
expect(exception_tracker).to receive(:capture_exception)
|
||||
|
||||
described_class.perform_now(imap_email_channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::SyncWidgetPreChatCustomFieldsJob do
|
||||
pre_chat_fields = [{
|
||||
'label' => 'Developer Id',
|
||||
'name' => 'developer_id'
|
||||
}, {
|
||||
'label' => 'Full Name',
|
||||
'name' => 'full_name'
|
||||
}]
|
||||
pre_chat_message = 'Share your queries here.'
|
||||
let!(:account) { create(:account) }
|
||||
let!(:web_widget) do
|
||||
create(:channel_widget, account: account, pre_chat_form_options: { pre_chat_message: pre_chat_message, pre_chat_fields: pre_chat_fields })
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'sync pre chat fields if custom attribute deleted' do
|
||||
described_class.perform_now(account, 'developer_id')
|
||||
expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [{
|
||||
'label' => 'Full Name',
|
||||
'name' => 'full_name'
|
||||
}]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::UpdateWidgetPreChatCustomFieldsJob do
|
||||
pre_chat_fields = [{
|
||||
'label' => 'Developer Id',
|
||||
'name' => 'developer_id'
|
||||
}, {
|
||||
'label' => 'Full Name',
|
||||
'name' => 'full_name'
|
||||
}]
|
||||
pre_chat_message = 'Share your queries here.'
|
||||
custom_attribute = {
|
||||
'attribute_key' => 'developer_id',
|
||||
'attribute_display_name' => 'Developer Number',
|
||||
'regex_pattern' => '^[0-9]*',
|
||||
'regex_cue' => 'It should be only digits'
|
||||
}
|
||||
let!(:account) { create(:account) }
|
||||
let!(:web_widget) do
|
||||
create(:channel_widget, account: account, pre_chat_form_options: { pre_chat_message: pre_chat_message, pre_chat_fields: pre_chat_fields })
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'sync pre chat fields if custom attribute updated' do
|
||||
described_class.perform_now(account, custom_attribute)
|
||||
expect(web_widget.reload.pre_chat_form_options['pre_chat_fields']).to eq [
|
||||
{ 'label' => 'Developer Number', 'name' => 'developer_id', 'placeholder' => 'Developer Number',
|
||||
'values' => nil, 'regex_pattern' => '^[0-9]*', 'regex_cue' => 'It should be only digits' },
|
||||
{ 'label' => 'Full Name', 'name' => 'full_name' }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
14
spec/jobs/internal/check_new_versions_job_spec.rb
Normal file
14
spec/jobs/internal/check_new_versions_job_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::CheckNewVersionsJob do
|
||||
subject(:job) { described_class.perform_now }
|
||||
|
||||
it 'updates the latest chatwoot version in redis' do
|
||||
data = { 'version' => '1.2.3' }
|
||||
allow(Rails.env).to receive(:production?).and_return(true)
|
||||
allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
|
||||
job
|
||||
expect(ChatwootHub).to have_received(:sync_with_hub)
|
||||
expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq data['version']
|
||||
end
|
||||
end
|
||||
44
spec/jobs/internal/delete_accounts_job_spec.rb
Normal file
44
spec/jobs/internal/delete_accounts_job_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::DeleteAccountsJob do
|
||||
subject(:job) { described_class.perform_later }
|
||||
|
||||
let!(:account_marked_for_deletion) { create(:account) }
|
||||
let!(:future_deletion_account) { create(:account) }
|
||||
let!(:active_account) { create(:account) }
|
||||
let(:account_deletion_service) { instance_double(AccountDeletionService, perform: true) }
|
||||
|
||||
before do
|
||||
account_marked_for_deletion.update!(
|
||||
custom_attributes: {
|
||||
'marked_for_deletion_at' => 1.day.ago.iso8601,
|
||||
'marked_for_deletion_reason' => 'user_requested'
|
||||
}
|
||||
)
|
||||
|
||||
future_deletion_account.update!(
|
||||
custom_attributes: {
|
||||
'marked_for_deletion_at' => 3.days.from_now.iso8601,
|
||||
'marked_for_deletion_reason' => 'user_requested'
|
||||
}
|
||||
)
|
||||
|
||||
allow(AccountDeletionService).to receive(:new).and_return(account_deletion_service)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'calls AccountDeletionService for accounts past deletion date' do
|
||||
described_class.new.perform
|
||||
|
||||
expect(AccountDeletionService).to have_received(:new).with(account: account_marked_for_deletion)
|
||||
expect(AccountDeletionService).not_to have_received(:new).with(account: future_deletion_account)
|
||||
expect(AccountDeletionService).not_to have_received(:new).with(account: active_account)
|
||||
expect(account_deletion_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/jobs/internal/process_stale_contacts_job_spec.rb
Normal file
64
spec/jobs/internal/process_stale_contacts_job_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::ProcessStaleContactsJob do
|
||||
subject(:job) { described_class.perform_later }
|
||||
|
||||
context 'when in cloud environment' do
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
end
|
||||
|
||||
it 'processes accounts based on the day of month' do
|
||||
# Set a fixed day for testing
|
||||
day_of_month = 16
|
||||
remainder = day_of_month % described_class::DISTRIBUTION_GROUPS
|
||||
allow(Date).to receive(:current).and_return(Date.new(2025, 5, day_of_month))
|
||||
|
||||
# Create an account and set its ID to match today's pattern
|
||||
account = create(:account)
|
||||
allow(account).to receive(:id).and_return(remainder)
|
||||
|
||||
# Mock the Account.where to return our filtered accounts
|
||||
account_relation = double
|
||||
allow(Account).to receive(:where).with("id % #{described_class::DISTRIBUTION_GROUPS} = ?", remainder).and_return(account_relation)
|
||||
allow(account_relation).to receive(:find_each).and_yield(account)
|
||||
|
||||
# Mock the delay setting
|
||||
allow(Internal::RemoveStaleContactsJob).to receive(:set).and_return(Internal::RemoveStaleContactsJob)
|
||||
expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'adds a delay between jobs' do
|
||||
day_of_month = 15
|
||||
remainder = day_of_month % described_class::DISTRIBUTION_GROUPS
|
||||
allow(Date).to receive(:current).and_return(Date.new(2025, 5, day_of_month))
|
||||
|
||||
account = create(:account)
|
||||
|
||||
account_relation = double
|
||||
allow(Account).to receive(:where).with("id % #{described_class::DISTRIBUTION_GROUPS} = ?", remainder).and_return(account_relation)
|
||||
allow(account_relation).to receive(:find_each).and_yield(account)
|
||||
|
||||
expect(Internal::RemoveStaleContactsJob).to receive(:set) do |args|
|
||||
expect(args[:wait]).to be_between(1.minute, 10.minutes)
|
||||
Internal::RemoveStaleContactsJob
|
||||
end
|
||||
expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not in cloud environment' do
|
||||
it 'does not process any accounts' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
|
||||
expect(Account).not_to receive(:where)
|
||||
expect(Internal::RemoveStaleContactsJob).not_to receive(:perform_later)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
15
spec/jobs/internal/process_stale_redis_keys_job_spec.rb
Normal file
15
spec/jobs/internal/process_stale_redis_keys_job_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::ProcessStaleRedisKeysJob do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'calls the RemoveStaleRedisKeysService with the correct account ID' do
|
||||
expect(Internal::RemoveStaleRedisKeysService).to receive(:new)
|
||||
.with(account_id: account.id)
|
||||
.and_call_original
|
||||
|
||||
described_class.perform_now(account)
|
||||
end
|
||||
end
|
||||
end
|
||||
20
spec/jobs/internal/remove_stale_contacts_job_spec.rb
Normal file
20
spec/jobs/internal/remove_stale_contacts_job_spec.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::RemoveStaleContactsJob do
|
||||
subject(:job) { described_class.perform_later(account) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(account)
|
||||
.on_queue('housekeeping')
|
||||
end
|
||||
|
||||
it 'calls the RemoveStaleContactsService' do
|
||||
service = instance_double(Internal::RemoveStaleContactsService)
|
||||
expect(Internal::RemoveStaleContactsService).to receive(:new).with(account: account).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(account)
|
||||
end
|
||||
end
|
||||
13
spec/jobs/internal/remove_stale_redis_keys_job_spec.rb
Normal file
13
spec/jobs/internal/remove_stale_redis_keys_job_spec.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Internal::RemoveStaleRedisKeysJob do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'enqueues ProcessStaleRedisKeysJob for the account' do
|
||||
expect(Internal::ProcessStaleRedisKeysJob).to receive(:perform_later).with(account)
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
15
spec/jobs/labels/update_job_spec.rb
Normal file
15
spec/jobs/labels/update_job_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Labels::UpdateJob do
|
||||
subject(:job) { described_class.perform_later(new_label_title, old_label_title, account_id) }
|
||||
|
||||
let(:new_label_title) { 'new-title' }
|
||||
let(:old_label_title) { 'old-title' }
|
||||
let(:account_id) { 1 }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(new_label_title, old_label_title, account_id)
|
||||
.on_queue('default')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Migration::ConversationsFirstReplySchedulerJob do
|
||||
subject(:job) { described_class.perform_later(account) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
.with(account)
|
||||
end
|
||||
|
||||
context 'when there is an outgoing message in conversation' do
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
|
||||
let!(:message) do
|
||||
create(:message, content: 'Hi', message_type: 'outgoing', account: account, inbox: inbox,
|
||||
conversation: conversation)
|
||||
end
|
||||
|
||||
it 'updates the conversation first reply with the first outgoing message created time' do
|
||||
create(:message, content: 'Hello', message_type: 'outgoing', account: account, inbox: inbox,
|
||||
conversation: conversation)
|
||||
|
||||
described_class.perform_now(account)
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq 2
|
||||
expect(conversation.first_reply_created_at.to_i).to eq message.created_at.to_i
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no outgoing message in conversation' do
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
|
||||
|
||||
it 'updates the conversation first reply with nil' do
|
||||
create(:message, content: 'Hello', message_type: 'incoming', account: account, inbox: inbox,
|
||||
conversation: conversation)
|
||||
|
||||
described_class.perform_now(account)
|
||||
conversation.reload
|
||||
|
||||
expect(conversation.messages.count).to eq 1
|
||||
expect(conversation.first_reply_created_at).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
10
spec/jobs/migration/remove_message_notifications_spec.rb
Normal file
10
spec/jobs/migration/remove_message_notifications_spec.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Migration::RemoveMessageNotifications do
|
||||
subject(:job) { described_class.perform_later }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
end
|
||||
58
spec/jobs/mutex_application_job_spec.rb
Normal file
58
spec/jobs/mutex_application_job_spec.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MutexApplicationJob do
|
||||
let(:lock_manager) { instance_double(Redis::LockManager) }
|
||||
let(:lock_key) { 'test_key' }
|
||||
|
||||
before do
|
||||
allow(Redis::LockManager).to receive(:new).and_return(lock_manager)
|
||||
allow(lock_manager).to receive(:lock).and_return(true)
|
||||
allow(lock_manager).to receive(:unlock).and_return(true)
|
||||
end
|
||||
|
||||
describe '#with_lock' do
|
||||
it 'acquires the lock and yields the block if lock is not acquired' do
|
||||
expect(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(true)
|
||||
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
|
||||
|
||||
expect { |b| described_class.new.send(:with_lock, lock_key, &b) }.to yield_control
|
||||
end
|
||||
|
||||
it 'acquires the lock with custom timeout' do
|
||||
expect(lock_manager).to receive(:lock).with(lock_key, 5.seconds).and_return(true)
|
||||
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
|
||||
|
||||
expect { |b| described_class.new.send(:with_lock, lock_key, 5.seconds, &b) }.to yield_control
|
||||
end
|
||||
|
||||
it 'raises LockAcquisitionError if it cannot acquire the lock' do
|
||||
allow(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(false)
|
||||
|
||||
expect do
|
||||
described_class.new.send(:with_lock, lock_key) do
|
||||
# Do nothing
|
||||
end
|
||||
end.to raise_error(StandardError) { |error| expect(error.class.name).to eq('MutexApplicationJob::LockAcquisitionError') }
|
||||
end
|
||||
|
||||
it 'raises StandardError if it execution raises it' do
|
||||
allow(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(false)
|
||||
allow(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
|
||||
|
||||
expect do
|
||||
described_class.new.send(:with_lock, lock_key) do
|
||||
raise StandardError
|
||||
end
|
||||
end.to raise_error(StandardError)
|
||||
end
|
||||
|
||||
it 'ensures that the lock is released even if there is an error during block execution' do
|
||||
expect(lock_manager).to receive(:lock).with(lock_key, Redis::LockManager::LOCK_TIMEOUT).and_return(true)
|
||||
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
|
||||
|
||||
expect do
|
||||
described_class.new.send(:with_lock, lock_key) { raise StandardError }
|
||||
end.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
38
spec/jobs/notification/delete_notification_job_spec.rb
Normal file
38
spec/jobs/notification/delete_notification_job_spec.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Notification::DeleteNotificationJob do
|
||||
let(:user) { create(:user) }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
context 'when enqueuing the job' do
|
||||
it 'enqueues the job to delete all notifications' do
|
||||
expect do
|
||||
described_class.perform_later(user.id, type: :all)
|
||||
end.to have_enqueued_job(described_class).on_queue('low')
|
||||
end
|
||||
|
||||
it 'enqueues the job to delete read notifications' do
|
||||
expect do
|
||||
described_class.perform_later(user.id, type: :read)
|
||||
end.to have_enqueued_job(described_class).on_queue('low')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when performing the job' do
|
||||
before do
|
||||
create(:notification, user: user, read_at: nil)
|
||||
create(:notification, user: user, read_at: Time.current)
|
||||
end
|
||||
|
||||
it 'deletes all notifications' do
|
||||
described_class.perform_now(user, type: :all)
|
||||
expect(user.notifications.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'deletes only read notifications' do
|
||||
described_class.perform_now(user, type: :read)
|
||||
expect(user.notifications.count).to eq(1)
|
||||
expect(user.notifications.where(read_at: nil).count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Notification::RemoveDuplicateNotificationJob do
|
||||
let(:user) { create(:user) }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
|
||||
expect do
|
||||
described_class.perform_later(duplicate_notification)
|
||||
end.to have_enqueued_job(described_class)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
it 'removes duplicate notifications' do
|
||||
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
|
||||
duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
|
||||
|
||||
described_class.perform_now(duplicate_notification)
|
||||
expect(Notification.count).to eq(1)
|
||||
end
|
||||
end
|
||||
23
spec/jobs/notification/remove_old_notification_job_spec.rb
Normal file
23
spec/jobs/notification/remove_old_notification_job_spec.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Notification::RemoveOldNotificationJob do
|
||||
let(:user) { create(:user) }
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect do
|
||||
described_class.perform_later
|
||||
end.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'removes old notifications which are older than 1 month' do
|
||||
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 2.months.ago)
|
||||
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.month.ago)
|
||||
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.day.ago)
|
||||
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.hour.ago)
|
||||
|
||||
described_class.perform_now
|
||||
expect(Notification.count).to eq(2)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Notification::ReopenSnoozedNotificationsJob do
|
||||
let!(:snoozed_till_5_minutes_ago) { create(:notification, snoozed_until: 5.minutes.ago) }
|
||||
let!(:snoozed_till_tomorrow) { create(:notification, snoozed_until: 1.day.from_now) }
|
||||
let!(:snoozed_indefinitely) { create(:notification) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'reopens snoozed notifications whose snooze until has passed' do
|
||||
described_class.perform_now
|
||||
|
||||
snoozed_until = snoozed_till_5_minutes_ago.reload.snoozed_until
|
||||
|
||||
expect(snoozed_till_5_minutes_ago.reload.snoozed_until).to be_nil
|
||||
expect(snoozed_till_tomorrow.reload.snoozed_until.to_date).to eq 1.day.from_now.to_date
|
||||
expect(snoozed_indefinitely.reload.snoozed_until).to be_nil
|
||||
expect(snoozed_indefinitely.reload.read_at).to be_nil
|
||||
expect(snoozed_until).to eq(snoozed_till_5_minutes_ago.reload.meta['snoozed_until'])
|
||||
end
|
||||
end
|
||||
end
|
||||
35
spec/jobs/send_on_slack_job_spec.rb
Normal file
35
spec/jobs/send_on_slack_job_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendOnSlackJob do
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:event_name) { 'message.created' }
|
||||
let(:event_data) { { message: create(:message, account: account, content: 'muchas muchas gracias', message_type: :incoming) } }
|
||||
|
||||
context 'when handleable events like message.created' do
|
||||
let(:process_service) { double }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
||||
allow(process_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'calls Integrations::Slack::SendOnSlackService when its a slack hook' do
|
||||
hook = create(:integrations_hook, app_id: 'slack', account: account)
|
||||
slack_service_instance = Integrations::Slack::SendOnSlackService.new(message: event_data[:message], hook: hook)
|
||||
expect(Integrations::Slack::SendOnSlackService).to receive(:new).with(message: event_data[:message],
|
||||
hook: hook).and_return(slack_service_instance)
|
||||
described_class.perform_now(event_data[:message], hook)
|
||||
end
|
||||
|
||||
it 'calls Integrations::Slack::SendOnSlackService when its a slack hook for template message' do
|
||||
event_data = { message: create(:message, account: account, message_type: :template) }
|
||||
hook = create(:integrations_hook, app_id: 'slack', account: account)
|
||||
slack_service_instance = Integrations::Slack::SendOnSlackService.new(message: event_data[:message], hook: hook)
|
||||
expect(Integrations::Slack::SendOnSlackService).to receive(:new).with(message: event_data[:message],
|
||||
hook: hook).and_return(slack_service_instance)
|
||||
described_class.perform_now(event_data[:message], hook)
|
||||
end
|
||||
end
|
||||
end
|
||||
148
spec/jobs/send_reply_job_spec.rb
Normal file
148
spec/jobs/send_reply_job_spec.rb
Normal file
@@ -0,0 +1,148 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendReplyJob do
|
||||
subject(:job) { described_class.perform_later(message) }
|
||||
|
||||
let(:message) { create(:message) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(message)
|
||||
.on_queue('high')
|
||||
end
|
||||
|
||||
context 'when the job is triggered on a new message' do
|
||||
let(:process_service) { double }
|
||||
|
||||
before do
|
||||
allow(process_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'calls Facebook::SendOnFacebookService when its facebook message' do
|
||||
stub_request(:post, /graph.facebook.com/)
|
||||
facebook_channel = create(:channel_facebook_page)
|
||||
facebook_inbox = create(:inbox, channel: facebook_channel)
|
||||
message = create(:message, conversation: create(:conversation, inbox: facebook_inbox))
|
||||
allow(Facebook::SendOnFacebookService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Facebook::SendOnFacebookService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Twitter::SendOnTwitterService when its twitter message' do
|
||||
twitter_channel = create(:channel_twitter_profile)
|
||||
twitter_inbox = create(:inbox, channel: twitter_channel)
|
||||
message = create(:message, conversation: create(:conversation, inbox: twitter_inbox))
|
||||
allow(Twitter::SendOnTwitterService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Twitter::SendOnTwitterService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Twilio::SendOnTwilioService when its twilio message' do
|
||||
twilio_channel = create(:channel_twilio_sms)
|
||||
message = create(:message, conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||
allow(Twilio::SendOnTwilioService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Twilio::SendOnTwilioService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Telegram::SendOnTelegramService when its telegram message' do
|
||||
telegram_channel = create(:channel_telegram)
|
||||
message = create(:message, conversation: create(:conversation, inbox: telegram_channel.inbox))
|
||||
allow(Telegram::SendOnTelegramService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Telegram::SendOnTelegramService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Line:SendOnLineService when its line message' do
|
||||
line_channel = create(:channel_line)
|
||||
message = create(:message, conversation: create(:conversation, inbox: line_channel.inbox))
|
||||
allow(Line::SendOnLineService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Line::SendOnLineService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Whatsapp:SendOnWhatsappService when its whatsapp message' do
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
whatsapp_channel = create(:channel_whatsapp, sync_templates: false)
|
||||
message = create(:message, conversation: create(:conversation, inbox: whatsapp_channel.inbox))
|
||||
allow(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Whatsapp::SendOnWhatsappService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Sms::SendOnSmsService when its sms message' do
|
||||
sms_channel = create(:channel_sms)
|
||||
message = create(:message, conversation: create(:conversation, inbox: sms_channel.inbox))
|
||||
allow(Sms::SendOnSmsService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Sms::SendOnSmsService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Instagram::Direct::SendOnInstagramService when its instagram message' do
|
||||
instagram_channel = create(:channel_instagram)
|
||||
message = create(:message, conversation: create(:conversation, inbox: instagram_channel.inbox))
|
||||
allow(Instagram::SendOnInstagramService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Instagram::SendOnInstagramService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Instagram::Messenger::SendOnInstagramService when its an instagram_direct_message from facebook channel' do
|
||||
stub_request(:post, /graph.facebook.com/)
|
||||
facebook_channel = create(:channel_facebook_page)
|
||||
facebook_inbox = create(:inbox, channel: facebook_channel)
|
||||
conversation = create(:conversation,
|
||||
inbox: facebook_inbox,
|
||||
additional_attributes: { 'type' => 'instagram_direct_message' })
|
||||
message = create(:message, conversation: conversation)
|
||||
|
||||
allow(Instagram::Messenger::SendOnInstagramService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Instagram::Messenger::SendOnInstagramService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Email::SendOnEmailService when its email message' do
|
||||
email_channel = create(:channel_email)
|
||||
message = create(:message, conversation: create(:conversation, inbox: email_channel.inbox))
|
||||
allow(Email::SendOnEmailService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Email::SendOnEmailService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Messages::SendEmailNotificationService when its webwidget message' do
|
||||
webwidget_channel = create(:channel_widget)
|
||||
message = create(:message, conversation: create(:conversation, inbox: webwidget_channel.inbox))
|
||||
allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Messages::SendEmailNotificationService when its api channel message' do
|
||||
api_channel = create(:channel_api)
|
||||
message = create(:message, conversation: create(:conversation, inbox: api_channel.inbox))
|
||||
allow(Messages::SendEmailNotificationService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Messages::SendEmailNotificationService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
|
||||
it 'calls ::Tiktok::SendOnTiktokService when its tiktok message' do
|
||||
tiktok_channel = create(:channel_tiktok)
|
||||
message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox))
|
||||
allow(Tiktok::SendOnTiktokService).to receive(:new).with(message: message).and_return(process_service)
|
||||
expect(Tiktok::SendOnTiktokService).to receive(:new).with(message: message)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(message.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
105
spec/jobs/slack_unfurl_job_spec.rb
Normal file
105
spec/jobs/slack_unfurl_job_spec.rb
Normal file
@@ -0,0 +1,105 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SlackUnfurlJob do
|
||||
subject(:job) { described_class.perform_later(params: link_shared, integration_hook: hook) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let!(:conversation) { create(:conversation, inbox: inbox) }
|
||||
let(:slack_client) { double }
|
||||
let(:link_shared) do
|
||||
{
|
||||
team_id: 'TLST3048H',
|
||||
api_app_id: 'A012S5UETV4',
|
||||
event: {
|
||||
type: 'link_shared',
|
||||
user: 'ULYPAKE5S',
|
||||
source: 'conversations_history',
|
||||
unfurl_id: 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
|
||||
channel: 'G01354F6A6Q',
|
||||
links: [{
|
||||
url: "https://qa.chatwoot.com/app/accounts/#{hook.account_id}/conversations/#{conversation.display_id}",
|
||||
domain: 'qa.chatwoot.com'
|
||||
}]
|
||||
},
|
||||
type: 'event_callback',
|
||||
event_time: 1_588_623_033
|
||||
}
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when the calls the slack unfurl job' do
|
||||
let(:slack_link_unfurl_service) { instance_double(Integrations::Slack::SlackLinkUnfurlService) }
|
||||
|
||||
before do
|
||||
allow(Integrations::Slack::SlackLinkUnfurlService).to receive(:new)
|
||||
.with(params: link_shared, integration_hook: hook)
|
||||
.and_return(slack_link_unfurl_service)
|
||||
end
|
||||
|
||||
context 'when the URL is shared in the channel' do
|
||||
let(:expected_body) { { channel: link_shared[:event][:channel] } }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'https://slack.com/api/conversations.members')
|
||||
.with(body: expected_body)
|
||||
.to_return(status: 200, body: { 'ok' => true }.to_json, headers: {})
|
||||
end
|
||||
|
||||
it 'does the unfurl' do
|
||||
expect(slack_link_unfurl_service).to receive(:perform)
|
||||
described_class.perform_now(link_shared)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the URL is shared in a different channel under the same account' do
|
||||
let(:expected_body) { { channel: 'XSDSFSFS' } }
|
||||
|
||||
before do
|
||||
link_shared[:event][:channel] = 'XSDSFSFS'
|
||||
stub_request(:post, 'https://slack.com/api/conversations.members')
|
||||
.with(body: expected_body)
|
||||
.to_return(status: 200, body: { 'ok' => true }.to_json, headers: {})
|
||||
end
|
||||
|
||||
it 'does the unfurl' do
|
||||
expect(slack_link_unfurl_service).to receive(:perform)
|
||||
described_class.perform_now(link_shared)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when another account URL is shared' do
|
||||
before do
|
||||
link_shared[:event][:links][0][:url] = 'https://qa.chatwoot.com/app/accounts/123/conversations/123'
|
||||
end
|
||||
|
||||
it 'does not unfurl' do
|
||||
expect(Integrations::Slack::SlackLinkUnfurlService).not_to receive(:new)
|
||||
expect(slack_link_unfurl_service).not_to receive(:perform)
|
||||
described_class.perform_now(link_shared)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the URL is shared in a channel under a different account' do
|
||||
let(:expected_body) { { channel: 'GDF4F6A6Q' } }
|
||||
|
||||
before do
|
||||
link_shared[:event][:channel] = 'GDF4F6A6Q'
|
||||
stub_request(:post, 'https://slack.com/api/conversations.members')
|
||||
.with(body: expected_body)
|
||||
.to_return(status: 404, body: { 'ok' => false }.to_json, headers: {})
|
||||
end
|
||||
|
||||
it 'does not unfurl' do
|
||||
expect(Integrations::Slack::SlackLinkUnfurlService).not_to receive(:new)
|
||||
expect(slack_link_unfurl_service).not_to receive(:perform)
|
||||
described_class.perform_now(link_shared)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
spec/jobs/trigger_scheduled_items_job_spec.rb
Normal file
49
spec/jobs/trigger_scheduled_items_job_spec.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TriggerScheduledItemsJob do
|
||||
subject(:job) { described_class.perform_later }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
it 'triggers Conversations::ReopenSnoozedConversationsJob' do
|
||||
expect(Conversations::ReopenSnoozedConversationsJob).to receive(:perform_later).once
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'triggers Notification::ReopenSnoozedNotificationsJob' do
|
||||
expect(Notification::ReopenSnoozedNotificationsJob).to receive(:perform_later).once
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'triggers Account::ConversationsResolutionSchedulerJob' do
|
||||
expect(Account::ConversationsResolutionSchedulerJob).to receive(:perform_later).once
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'triggers Channels::Whatsapp::TemplatesSyncSchedulerJob' do
|
||||
expect(Channels::Whatsapp::TemplatesSyncSchedulerJob).to receive(:perform_later).once
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'triggers Notification::RemoveOldNotificationJob' do
|
||||
expect(Notification::RemoveOldNotificationJob).to receive(:perform_later).once
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
context 'when unexecuted Scheduled campaign jobs' do
|
||||
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
||||
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||
|
||||
it 'triggers Campaigns::TriggerOneoffCampaignJob' do
|
||||
campaign = create(:campaign, inbox: twilio_inbox, account: account)
|
||||
create(:campaign, inbox: twilio_inbox, account: account, scheduled_at: 10.days.after)
|
||||
expect(Campaigns::TriggerOneoffCampaignJob).to receive(:perform_later).with(campaign).once
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
31
spec/jobs/webhook_job_spec.rb
Normal file
31
spec/jobs/webhook_job_spec.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe WebhookJob do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
subject(:job) { described_class.perform_later(url, payload, webhook_type) }
|
||||
|
||||
let(:url) { 'https://test.chatwoot.com' }
|
||||
let(:payload) { { name: 'test' } }
|
||||
let(:webhook_type) { :account_webhook }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(url, payload, webhook_type)
|
||||
.on_queue('medium')
|
||||
end
|
||||
|
||||
it 'executes perform with default webhook type' do
|
||||
expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type)
|
||||
perform_enqueued_jobs { job }
|
||||
end
|
||||
|
||||
context 'with custom webhook type' do
|
||||
let(:webhook_type) { :api_inbox_webhook }
|
||||
|
||||
it 'executes perform with inbox webhook type' do
|
||||
expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type)
|
||||
perform_enqueued_jobs { job }
|
||||
end
|
||||
end
|
||||
end
|
||||
44
spec/jobs/webhooks/facebook_delivery_job_spec.rb
Normal file
44
spec/jobs/webhooks/facebook_delivery_job_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::FacebookDeliveryJob do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:message) { 'test_message' }
|
||||
let(:parsed_message) { instance_double(Integrations::Facebook::MessageParser) }
|
||||
let(:delivery_status) { instance_double(Integrations::Facebook::DeliveryStatus) }
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).with(message).and_return(parsed_message)
|
||||
allow(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message).and_return(delivery_status)
|
||||
allow(delivery_status).to receive(:perform)
|
||||
end
|
||||
|
||||
after do
|
||||
clear_enqueued_jobs
|
||||
end
|
||||
|
||||
describe '#perform_later' do
|
||||
it 'enqueues the job' do
|
||||
expect do
|
||||
described_class.perform_later(message)
|
||||
end.to have_enqueued_job(described_class).with(message).on_queue('low')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'calls the MessageParser with the correct argument' do
|
||||
expect(Integrations::Facebook::MessageParser).to receive(:new).with(message)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
|
||||
it 'calls the DeliveryStatus with the correct argument' do
|
||||
expect(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
|
||||
it 'executes perform on the DeliveryStatus instance' do
|
||||
expect(delivery_status).to receive(:perform)
|
||||
described_class.perform_now(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
51
spec/jobs/webhooks/facebook_events_job_spec.rb
Normal file
51
spec/jobs/webhooks/facebook_events_job_spec.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::FacebookEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) { { test: 'test' } }
|
||||
let(:parsed_response) { instance_double(Integrations::Facebook::MessageParser) }
|
||||
let(:lock_key_format) { Redis::Alfred::FACEBOOK_MESSAGE_MUTEX }
|
||||
let(:lock_key) { format(lock_key_format, sender_id: 'sender_id', recipient_id: 'recipient_id') } # Use real format if different
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).and_return(parsed_response)
|
||||
allow(parsed_response).to receive(:sender_id).and_return('sender_id')
|
||||
allow(parsed_response).to receive(:recipient_id).and_return('recipient_id')
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
describe 'job execution' do
|
||||
let(:message_creator) { instance_double(Integrations::Facebook::MessageCreator) }
|
||||
|
||||
before do
|
||||
allow(Integrations::Facebook::MessageParser).to receive(:new).and_return(parsed_response)
|
||||
allow(Integrations::Facebook::MessageCreator).to receive(:new).with(parsed_response).and_return(message_creator)
|
||||
allow(message_creator).to receive(:perform)
|
||||
end
|
||||
|
||||
# ensures that the response is built
|
||||
it 'invokes the message parser and creator' do
|
||||
expect(Integrations::Facebook::MessageParser).to receive(:new).with(params)
|
||||
expect(Integrations::Facebook::MessageCreator).to receive(:new).with(parsed_response)
|
||||
expect(message_creator).to receive(:perform)
|
||||
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
# this test ensures that the process message function is indeed called
|
||||
it 'attempts to acquire a lock and then processes the message' do
|
||||
job_instance = described_class.new
|
||||
allow(job_instance).to receive(:process_message).with(parsed_response)
|
||||
|
||||
job_instance.perform(params)
|
||||
|
||||
expect(job_instance).to have_received(:process_message).with(parsed_response)
|
||||
end
|
||||
end
|
||||
end
|
||||
387
spec/jobs/webhooks/instagram_events_job_spec.rb
Normal file
387
spec/jobs/webhooks/instagram_events_job_spec.rb
Normal file
@@ -0,0 +1,387 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Webhooks::InstagramEventsJob do
|
||||
subject(:instagram_webhook) { described_class }
|
||||
|
||||
before do
|
||||
stub_request(:post, /graph\.facebook\.com/)
|
||||
stub_request(:get, 'https://www.example.com/test.jpeg')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=17949487764033669&signature=test')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:get, 'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=18091626484740369&signature=test')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
|
||||
def return_object_for(sender_id)
|
||||
{ name: 'Jane',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
username: 'some_user_name' }
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when handling messaging events for Instagram via Facebook page' do
|
||||
let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) }
|
||||
let(:fb_object) { double }
|
||||
|
||||
it 'creates incoming message in the instagram inbox' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.content_attributes['is_unsupported']).to be_nil
|
||||
end
|
||||
|
||||
it 'creates standby message in the instagram inbox' do
|
||||
standby_event = build(:instagram_message_standby_event).with_indifferent_access
|
||||
sender_id = standby_event[:entry][0][:standby][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(standby_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
expect(message.content).to eq('This is the first standby message from the customer, after 24 hours.')
|
||||
end
|
||||
|
||||
it 'handle instagram unsend message event' do
|
||||
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
|
||||
sender_id = unsend_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete')
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: sender_id,
|
||||
account_id: instagram_messenger_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
message.attachments.new(file_type: :image, external_url: 'https://www.example.com/test.jpeg')
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
|
||||
instagram_webhook.perform_now(unsend_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.last.content).to eq 'This message was deleted'
|
||||
expect(instagram_messenger_inbox.messages.last.deleted).to be true
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 0
|
||||
expect(instagram_messenger_inbox.messages.last.reload.deleted).to be true
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox' do
|
||||
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
|
||||
sender_id = attachment_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(attachment_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox for story mention' do
|
||||
story_mention_event = build(:instagram_story_mention_event).with_indifferent_access
|
||||
sender_id = story_mention_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access,
|
||||
{ story:
|
||||
{
|
||||
mention: {
|
||||
link: 'https://www.example.com/test.jpeg',
|
||||
id: '17920786367196703'
|
||||
}
|
||||
},
|
||||
from: {
|
||||
username: 'Sender-id-1', id: 'Sender-id-1'
|
||||
},
|
||||
id: 'instagram-message-id-1234' }.with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
attachment = instagram_messenger_inbox.messages.last.attachments.last
|
||||
expect(attachment.push_event_data[:data_url]).to eq(attachment.external_url)
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_story attachment in the instagram inbox' do
|
||||
ig_story_event = build(:instagram_ig_story_event).with_indifferent_access
|
||||
sender_id = ig_story_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(ig_story_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_story'
|
||||
expect(attachment.external_url).to include 'lookaside.fbsbx.com'
|
||||
expect(message.content).to eq 'Shared story'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_story'
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_post attachment in the instagram inbox' do
|
||||
ig_post_event = build(:instagram_ig_post_event).with_indifferent_access
|
||||
sender_id = ig_post_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(ig_post_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_messenger_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_post'
|
||||
expect(attachment.external_url).to include 'ig_messaging_cdn'
|
||||
expect(message.content).to eq 'Shared post'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_post'
|
||||
end
|
||||
|
||||
it 'does not create contact or messages when Facebook API call fails' do
|
||||
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_echo_event[:entry])
|
||||
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 0
|
||||
expect(instagram_messenger_inbox.contact_inboxes.count).to be 0
|
||||
expect(instagram_messenger_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
it 'handle messaging_seen callback' do
|
||||
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||
|
||||
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||
channel: instagram_messenger_inbox.channel).and_call_original
|
||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||
end
|
||||
|
||||
it 'handles unsupported message' do
|
||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
|
||||
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_object_for(sender_id).with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(unsupported_event[:entry])
|
||||
expect(instagram_messenger_inbox.contacts.count).to be 1
|
||||
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_messenger_inbox.conversations.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.count).to be 1
|
||||
expect(instagram_messenger_inbox.messages.last.content_attributes['is_unsupported']).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling messaging events for Instagram via Instagram login' do
|
||||
let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_inbox) { instagram_channel.inbox }
|
||||
|
||||
before do
|
||||
instagram_channel.update(access_token: 'valid_instagram_token')
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-.*\?.*})
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: proc { |request|
|
||||
sender_id = request.uri.path.split('/').last.split('?').first
|
||||
{
|
||||
name: 'Jane',
|
||||
username: 'some_user_name',
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png',
|
||||
id: sender_id,
|
||||
follower_count: 100,
|
||||
is_user_follow_business: true,
|
||||
is_business_follow_user: true,
|
||||
is_verified_user: false
|
||||
}.to_json
|
||||
},
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates incoming message with correct contact info in the instagram direct inbox' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
expect(instagram_inbox.contacts.count).to eq 1
|
||||
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
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
|
||||
|
||||
it 'sets correct instagram attributes on contact' do
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
instagram_inbox.reload
|
||||
|
||||
contact = instagram_inbox.contacts.last
|
||||
|
||||
expect(contact.additional_attributes['social_instagram_follower_count']).to eq 100
|
||||
expect(contact.additional_attributes['social_instagram_is_user_follow_business']).to be true
|
||||
expect(contact.additional_attributes['social_instagram_is_business_follow_user']).to be true
|
||||
expect(contact.additional_attributes['social_instagram_is_verified_user']).to be false
|
||||
end
|
||||
|
||||
it 'handle instagram unsend message event' do
|
||||
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
|
||||
|
||||
message = create(:message, inbox_id: instagram_inbox.id, source_id: 'message-id-to-delete', content: 'random_text')
|
||||
|
||||
# Create attachment correctly with account association
|
||||
message.attachments.create!(
|
||||
file_type: :image,
|
||||
external_url: 'https://www.example.com/test.jpeg',
|
||||
account_id: instagram_inbox.account_id
|
||||
)
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
|
||||
instagram_webhook.perform_now(unsend_event[:entry])
|
||||
|
||||
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
|
||||
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(attachment_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
|
||||
it 'handles unsupported message' do
|
||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(unsupported_event[:entry])
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
|
||||
expect(instagram_inbox.conversations.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be true
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_story attachment in the instagram direct inbox' do
|
||||
ig_story_event = build(:instagram_ig_story_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(ig_story_event[:entry])
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_story'
|
||||
expect(attachment.external_url).to include 'lookaside.fbsbx.com'
|
||||
expect(message.content).to eq 'Shared story'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_story'
|
||||
end
|
||||
|
||||
it 'creates incoming message with ig_post attachment in the instagram direct inbox' do
|
||||
ig_post_event = build(:instagram_ig_post_event).with_indifferent_access
|
||||
instagram_webhook.perform_now(ig_post_event[:entry])
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
|
||||
message = instagram_inbox.messages.last
|
||||
attachment = message.attachments.last
|
||||
|
||||
expect(attachment.file_type).to eq 'ig_post'
|
||||
expect(attachment.external_url).to include 'ig_messaging_cdn'
|
||||
expect(message.content).to eq 'Shared post'
|
||||
expect(message.content_attributes['image_type']).to eq 'ig_post'
|
||||
end
|
||||
|
||||
it 'does not create contact or messages when Instagram API call fails' do
|
||||
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
|
||||
|
||||
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
|
||||
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_echo_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 0
|
||||
expect(instagram_inbox.contact_inboxes.count).to be 0
|
||||
expect(instagram_inbox.messages.count).to be 0
|
||||
end
|
||||
|
||||
it 'handles messaging_seen callback' do
|
||||
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||
|
||||
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||
channel: instagram_inbox.channel).and_call_original
|
||||
instagram_webhook.perform_now(messaging_seen_event[: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)
|
||||
|
||||
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
|
||||
instagram_webhook.perform_now(dm_event[:entry])
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})"
|
||||
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
|
||||
|
||||
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
|
||||
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
38
spec/jobs/webhooks/line_events_job_spec.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::LineEventsJob do
|
||||
subject(:job) { described_class.perform_later(params: params) }
|
||||
|
||||
let!(:line_channel) { create(:channel_line) }
|
||||
let!(:params) { { :line_channel_id => line_channel.line_channel_id, 'line' => { test: 'test' } } }
|
||||
let(:post_body) { params.to_json }
|
||||
let(:signature) { Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), line_channel.line_channel_secret, post_body)) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params: params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no line_channel_id' do
|
||||
expect(described_class.perform_now(params: {})).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when invalid bot_token' do
|
||||
expect(described_class.perform_now(params: { 'line_channel_id' => 'invalid_id', 'line' => { test: 'test' } })).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Line::IncomingMessageService' do
|
||||
process_service = double
|
||||
allow(Line::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Line::IncomingMessageService).to receive(:new).with(inbox: line_channel.inbox,
|
||||
params: params['line'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params: params, post_body: post_body, signature: signature)
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
85
spec/jobs/webhooks/sms_events_job_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::SmsEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let!(:sms_channel) { create(:channel_sms) }
|
||||
let!(:params) do
|
||||
{
|
||||
time: '2022-02-02T23:14:05.309Z',
|
||||
type: 'message-received',
|
||||
to: sms_channel.phone_number,
|
||||
description: 'Incoming message received',
|
||||
message: {
|
||||
'id': '3232420-2323-234324',
|
||||
'owner': sms_channel.phone_number,
|
||||
'applicationId': '2342349-324234d-32432432',
|
||||
'time': '2022-02-02T23:14:05.262Z',
|
||||
'segmentCount': 1,
|
||||
'direction': 'in',
|
||||
'to': [
|
||||
sms_channel.phone_number
|
||||
],
|
||||
'from': '+14234234234',
|
||||
'text': 'test message'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no bot_token' do
|
||||
expect(described_class.perform_now({})).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when invalid type' do
|
||||
expect(described_class.perform_now({ type: 'invalid' })).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Sms::IncomingMessageService if the message type is message-received' do
|
||||
process_service = double
|
||||
allow(Sms::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::IncomingMessageService).to receive(:new).with(inbox: sms_channel.inbox,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'calls Sms::DeliveryStatusService if the message type is message-delivered' do
|
||||
params[:type] = 'message-delivered'
|
||||
process_service = double
|
||||
allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'calls Sms::DeliveryStatusService if the message type is message-failed' do
|
||||
params[:type] = 'message-failed'
|
||||
process_service = double
|
||||
allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel,
|
||||
params: params[:message].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
it 'does not call any service if the message type is not supported' do
|
||||
params[:type] = 'message-sent'
|
||||
expect(Sms::IncomingMessageService).not_to receive(:new)
|
||||
expect(Sms::DeliveryStatusService).not_to receive(:new)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/jobs/webhooks/telegram_events_job_spec.rb
Normal file
64
spec/jobs/webhooks/telegram_events_job_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TelegramEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let!(:telegram_channel) { create(:channel_telegram) }
|
||||
let!(:params) { { :bot_token => telegram_channel.bot_token, 'telegram' => { test: 'test' } } }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('default')
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
it 'returns nil when no bot_token' do
|
||||
expect(described_class.perform_now({})).to be_nil
|
||||
end
|
||||
|
||||
it 'logs a warning when channel is not found' do
|
||||
expect(Rails.logger).to receive(:warn).with('Telegram event discarded: Channel not found for bot_token: invalid')
|
||||
described_class.perform_now({ bot_token: 'invalid' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid params' do
|
||||
it 'calls Telegram::IncomingMessageService' do
|
||||
process_service = double
|
||||
allow(Telegram::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Telegram::IncomingMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||
params: params['telegram'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'logs a warning and does not process events if account is suspended' do
|
||||
account = telegram_channel.account
|
||||
account.update!(status: :suspended)
|
||||
|
||||
process_service = double
|
||||
allow(Telegram::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Telegram event discarded: Account #{account.id} is not active for channel #{telegram_channel.id}")
|
||||
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update message params' do
|
||||
let!(:params) { { :bot_token => telegram_channel.bot_token, 'telegram' => { edited_message: 'test' } } }
|
||||
|
||||
it 'calls Telegram::UpdateMessageService' do
|
||||
process_service = double
|
||||
allow(Telegram::UpdateMessageService).to receive(:new).and_return(process_service)
|
||||
allow(process_service).to receive(:perform)
|
||||
expect(Telegram::UpdateMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||
params: params['telegram'].with_indifferent_access)
|
||||
expect(process_service).to receive(:perform)
|
||||
described_class.perform_now(params.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
end
|
||||
91
spec/jobs/webhooks/tiktok_events_job_spec.rb
Normal file
91
spec/jobs/webhooks/tiktok_events_job_spec.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TiktokEventsJob do
|
||||
let(:account) { create(:account) }
|
||||
let!(:channel) { create(:channel_tiktok, account: account, business_id: 'biz-123') }
|
||||
let(:job) { described_class.new }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:with_lock).and_yield
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'processes im_receive_msg events via Tiktok::MessageService' do
|
||||
message_service = instance_double(Tiktok::MessageService, perform: true)
|
||||
allow(Tiktok::MessageService).to receive(:new).and_return(message_service)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||
expect(message_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'processes im_mark_read_msg events via Tiktok::ReadStatusService' do
|
||||
read_status_service = instance_double(Tiktok::ReadStatusService, perform: true)
|
||||
allow(Tiktok::ReadStatusService).to receive(:new).and_return(read_status_service)
|
||||
|
||||
event = {
|
||||
event: 'im_mark_read_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1', read: { last_read_timestamp: 1_700_000_000_000 }, from_user: { id: 'user-1' } }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::ReadStatusService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||
expect(read_status_service).to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'ignores unsupported event types' do
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
event = {
|
||||
event: 'unknown_event',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does nothing when channel is missing' do
|
||||
allow(Tiktok::MessageService).to receive(:new)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-does-not-exist',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does nothing when account is inactive' do
|
||||
allow(Channel::Tiktok).to receive(:find_by).and_return(channel)
|
||||
allow(channel.account).to receive(:active?).and_return(false)
|
||||
|
||||
message_service = instance_double(Tiktok::MessageService, perform: true)
|
||||
allow(Tiktok::MessageService).to receive(:new).and_return(message_service)
|
||||
|
||||
event = {
|
||||
event: 'im_receive_msg',
|
||||
user_openid: 'biz-123',
|
||||
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||
}
|
||||
|
||||
job.perform(event)
|
||||
|
||||
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
end
|
||||
26
spec/jobs/webhooks/twilio_delivery_status_job_spec.rb
Normal file
26
spec/jobs/webhooks/twilio_delivery_status_job_spec.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TwilioDeliveryStatusJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
'MessageSid' => 'SM123',
|
||||
'MessageStatus' => 'delivered',
|
||||
'AccountSid' => 'AC123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'calls the Twilio::DeliveryStatusService' do
|
||||
service = double
|
||||
expect(Twilio::DeliveryStatusService).to receive(:new).with(params: params).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.new.perform(params)
|
||||
end
|
||||
end
|
||||
103
spec/jobs/webhooks/twilio_events_job_spec.rb
Normal file
103
spec/jobs/webhooks/twilio_events_job_spec.rb
Normal file
@@ -0,0 +1,103 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::TwilioEventsJob do
|
||||
subject(:job) { described_class.perform_later(params) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
Body: 'Test message',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
it 'calls the Twilio::IncomingMessageService' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params)
|
||||
end
|
||||
|
||||
context 'when Body parameter or MediaUrl0 is not present' do
|
||||
let(:params_without_body) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not process the event' do
|
||||
expect(Twilio::IncomingMessageService).not_to receive(:new)
|
||||
described_class.perform_now(params_without_body)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Body parameter is present' do
|
||||
let(:params_with_body) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
Body: 'Test message',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the event' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_body).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_body)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when MediaUrl0 parameter is present' do
|
||||
let(:params_with_media) do
|
||||
{
|
||||
From: '+1234567890',
|
||||
To: '+0987654321',
|
||||
MediaUrl0: 'https://example.com/media.jpg',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the event' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_media).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_media)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when location message is present' do
|
||||
let(:params_with_location) do
|
||||
{
|
||||
From: 'whatsapp:+1234567890',
|
||||
To: 'whatsapp:+0987654321',
|
||||
MessageType: 'location',
|
||||
Latitude: '12.160894393921',
|
||||
Longitude: '75.265205383301',
|
||||
AccountSid: 'AC123',
|
||||
SmsSid: 'SM123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the location message' do
|
||||
service = double
|
||||
expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_location).and_return(service)
|
||||
expect(service).to receive(:perform)
|
||||
described_class.perform_now(params_with_location)
|
||||
end
|
||||
end
|
||||
end
|
||||
246
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
246
spec/jobs/webhooks/whatsapp_events_job_spec.rb
Normal file
@@ -0,0 +1,246 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Webhooks::WhatsappEventsJob do
|
||||
subject(:job) { described_class }
|
||||
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let(:params) do
|
||||
{
|
||||
object: 'whatsapp_business_account',
|
||||
phone_number: channel.phone_number,
|
||||
entry: [{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: channel.provider_config['phone_number_id'],
|
||||
display_phone_number: channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
end
|
||||
let(:process_service) { double }
|
||||
|
||||
before do
|
||||
allow(process_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job.perform_later(params) }.to have_enqueued_job(described_class)
|
||||
.with(params)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when whatsapp_cloud provider' do
|
||||
it 'enqueue Whatsapp::IncomingMessageWhatsappCloudService' do
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue message jobs based on phone number in the URL if the entry payload is not present' do
|
||||
params = {
|
||||
object: 'whatsapp_business_account',
|
||||
phone_number: channel.phone_number,
|
||||
entry: [{ changes: [{}] }]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue Whatsapp::IncomingMessageWhatsappCloudService if channel reauthorization required' do
|
||||
channel.prompt_reauthorization!
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'will not enqueue if channel is not present' do
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(phone_number: 'random_phone_number')
|
||||
end
|
||||
|
||||
it 'will not enqueue Whatsapp::IncomingMessageWhatsappCloudService if account is suspended' do
|
||||
account = channel.account
|
||||
account.update!(status: :suspended)
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||
expect(Whatsapp::IncomingMessageService).not_to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'logs a warning when channel is inactive' do
|
||||
channel.prompt_reauthorization!
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Inactive WhatsApp channel: #{channel.phone_number}")
|
||||
job.perform_now(params)
|
||||
end
|
||||
|
||||
it 'logs a warning with unknown phone number when channel does not exist' do
|
||||
unknown_phone = '+1234567890'
|
||||
allow(Rails.logger).to receive(:warn)
|
||||
|
||||
expect(Rails.logger).to receive(:warn).with("Inactive WhatsApp channel: unknown - #{unknown_phone}")
|
||||
job.perform_now(phone_number: unknown_phone)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when default provider' do
|
||||
it 'enqueue Whatsapp::IncomingMessageService' do
|
||||
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
|
||||
channel.update(provider: 'default')
|
||||
allow(Whatsapp::IncomingMessageService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageService).to receive(:new)
|
||||
job.perform_now(params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when whatsapp business params' do
|
||||
it 'enqueue Whatsapp::IncomingMessageWhatsappCloudService based on the number in payload' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [
|
||||
{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).with(inbox: other_channel.inbox, params: wb_params)
|
||||
job.perform_now(wb_params)
|
||||
end
|
||||
|
||||
it 'Ignore reaction type message and stop raising error' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||
messages: [{
|
||||
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
|
||||
it 'ignore reaction type message, would not create contact if the reaction is the first event' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||
messages: [{
|
||||
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Contact, :count)
|
||||
end
|
||||
|
||||
it 'ignore request_welcome type message, would not create contact or conversation' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
messages: [{
|
||||
from: '1111981136571', timestamp: '1664799904', type: 'request_welcome'
|
||||
}],
|
||||
metadata: {
|
||||
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}.with_indifferent_access
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Contact, :count)
|
||||
|
||||
expect do
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||
end.not_to change(Conversation, :count)
|
||||
end
|
||||
|
||||
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
|
||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||
validate_provider_config: false)
|
||||
wb_params = {
|
||||
phone_number: channel.phone_number,
|
||||
object: 'whatsapp_business_account',
|
||||
entry: [
|
||||
{
|
||||
changes: [
|
||||
{
|
||||
value: {
|
||||
metadata: {
|
||||
phone_number_id: 'random phone number id',
|
||||
display_phone_number: other_channel.phone_number.delete('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new).with(inbox: other_channel.inbox, params: wb_params)
|
||||
job.perform_now(wb_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user