From 7930902ec890ebc7ea15cbcbc4325bca0a04fb3d Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 30 Mar 2023 21:02:52 +0530 Subject: [PATCH] chore: Refactor Automation Specs (#6796) The current way of writing specs for automation in one single file automation_listener isn't effective. Hence we are breaking down the specs for each class into separate spec files. fixes: CW-1447 --- app/listeners/automation_rule_listener.rb | 42 +- .../automation_rules/action_service.rb | 6 +- spec/factories/automation_rules.rb | 1 + .../automation_rule_listener_old_spec.rb | 896 ++++++++++++++++ .../automation_rule_listener_spec.rb | 981 +++--------------- spec/services/action_service_spec.rb | 15 + .../automation_rules/action_service_spec.rb | 91 ++ .../conditions_filter_service_spec.rb | 29 + 8 files changed, 1184 insertions(+), 877 deletions(-) create mode 100644 spec/listeners/automation_rule_listener_old_spec.rb create mode 100644 spec/services/action_service_spec.rb create mode 100644 spec/services/automation_rules/action_service_spec.rb create mode 100644 spec/services/automation_rules/conditions_filter_service_spec.rb diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index 3a88963a2..5dabda47e 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -1,10 +1,10 @@ class AutomationRuleListener < BaseListener - def conversation_updated(event_obj) - return if performed_by_automation?(event_obj) + def conversation_updated(event) + return if performed_by_automation?(event) - conversation = event_obj.data[:conversation] + conversation = event.data[:conversation] account = conversation.account - changed_attributes = event_obj.data[:changed_attributes] + changed_attributes = event.data[:changed_attributes] return unless rule_present?('conversation_updated', account) @@ -16,12 +16,12 @@ class AutomationRuleListener < BaseListener end end - def conversation_created(event_obj) - return if performed_by_automation?(event_obj) + def conversation_created(event) + return if performed_by_automation?(event) - conversation = event_obj.data[:conversation] + conversation = event.data[:conversation] account = conversation.account - changed_attributes = event_obj.data[:changed_attributes] + changed_attributes = event.data[:changed_attributes] return unless rule_present?('conversation_created', account) @@ -33,12 +33,12 @@ class AutomationRuleListener < BaseListener end end - def conversation_opened(event_obj) - return if performed_by_automation?(event_obj) + def conversation_opened(event) + return if performed_by_automation?(event) - conversation = event_obj.data[:conversation] + conversation = event.data[:conversation] account = conversation.account - changed_attributes = event_obj.data[:changed_attributes] + changed_attributes = event.data[:changed_attributes] return unless rule_present?('conversation_opened', account) @@ -50,13 +50,13 @@ class AutomationRuleListener < BaseListener end end - def message_created(event_obj) - message = event_obj.data[:message] + def message_created(event) + message = event.data[:message] - return if ignore_message_created_event?(event_obj) + return if ignore_message_created_event?(event) account = message.try(:account) - changed_attributes = event_obj.data[:changed_attributes] + changed_attributes = event.data[:changed_attributes] return unless rule_present?('message_created', account) @@ -83,12 +83,12 @@ class AutomationRuleListener < BaseListener ) end - def performed_by_automation?(event_obj) - event_obj.data[:performed_by].present? && event_obj.data[:performed_by].instance_of?(AutomationRule) + def performed_by_automation?(event) + event.data[:performed_by].present? && event.data[:performed_by].instance_of?(AutomationRule) end - def ignore_message_created_event?(event_obj) - message = event_obj.data[:message] - performed_by_automation?(event_obj) || message.activity? + def ignore_message_created_event?(event) + message = event.data[:message] + performed_by_automation?(event) || message.activity? end end diff --git a/app/services/automation_rules/action_service.rb b/app/services/automation_rules/action_service.rb index ad5439c1d..290e14a9e 100644 --- a/app/services/automation_rules/action_service.rb +++ b/app/services/automation_rules/action_service.rb @@ -32,8 +32,7 @@ class AutomationRules::ActionService < ActionService return if blobs.blank? params = { content: nil, private: false, attachments: blobs } - mb = Messages::MessageBuilder.new(nil, @conversation, params) - mb.perform + Messages::MessageBuilder.new(nil, @conversation, params).perform end def send_webhook_event(webhook_url) @@ -45,8 +44,7 @@ class AutomationRules::ActionService < ActionService return if conversation_a_tweet? params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } } - mb = Messages::MessageBuilder.new(nil, @conversation, params) - mb.perform + Messages::MessageBuilder.new(nil, @conversation, params).perform end def send_email_to_team(params) diff --git a/spec/factories/automation_rules.rb b/spec/factories/automation_rules.rb index 3a7aecd09..4a704566b 100644 --- a/spec/factories/automation_rules.rb +++ b/spec/factories/automation_rules.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :automation_rule do account + name { 'Test Automation Rule' } event_name { 'conversation_status_changed' } conditions { [{ 'values': ['resolved'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }] } actions do diff --git a/spec/listeners/automation_rule_listener_old_spec.rb b/spec/listeners/automation_rule_listener_old_spec.rb new file mode 100644 index 000000000..69154c592 --- /dev/null +++ b/spec/listeners/automation_rule_listener_old_spec.rb @@ -0,0 +1,896 @@ +# This file is deprecated, please don't add any additional specs or modifications in this +# Its much more effective to add specs for each idividual class in automation service +# please update 'automation_rule_listener_spec.rb', 'actions_filter_service_spec.rb and 'conditions_filter_service_spec.rb' instead +# Ensure the above mentioned files have parity for conditions tested here and then remove this file from code base. + +require 'rails_helper' +describe AutomationRuleListener do + let(:listener) { described_class.instance } + let!(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account, identifier: '123') } + let(:conversation) { create(:conversation, inbox: inbox, account: account) } + let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') } + let(:team) { create(:team, account: account) } + let(:user_1) { create(:user, role: 0) } + let(:user_2) { create(:user, role: 0) } + + before do + create(:custom_attribute_definition, + attribute_key: 'customer_type', + account: account, + attribute_model: 'contact_attribute', + attribute_display_type: 'list', + attribute_values: %w[regular platinum gold]) + create(:custom_attribute_definition, + attribute_key: 'priority', + account: account, + attribute_model: 'conversation_attribute', + attribute_display_type: 'list', + attribute_values: %w[P0 P1 P2]) + create(:custom_attribute_definition, + attribute_key: 'cloud_customer', + attribute_display_type: 'checkbox', + account: account, + attribute_model: 'contact_attribute') + create(:team_member, user: user_1, team: team) + create(:team_member, user: user_2, team: team) + create(:inbox_member, user: user_1, inbox: inbox) + create(:inbox_member, user: user_2, inbox: inbox) + create(:account_user, user: user_2, account: account) + create(:account_user, user: user_1, account: account) + + conversation.resolved! + automation_rule.update!(actions: + [ + { + 'action_name' => 'send_email_to_team', 'action_params' => [{ + 'message' => 'Please pay attention to this conversation, its from high priority customer', + 'team_ids' => [team.id] + }] + }, + { 'action_name' => 'assign_team', 'action_params' => [team.id] }, + { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, + { 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] }, + { 'action_name' => 'assign_agent', 'action_params' => [user_1.id] }, + { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }, + { 'action_name' => 'mute_conversation', 'action_params' => nil }, + { 'action_name' => 'change_status', 'action_params' => ['snoozed'] }, + { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, + { 'action_name' => 'send_attachment' } + ]) + file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') + automation_rule.files.attach(file) + automation_rule.save! + end + + describe '#conversation_updated with contacts attributes' do + before do + conversation.contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' }, + additional_attributes: { 'company': 'Marvel' }) + + automation_rule.update!( + event_name: 'conversation_updated', + name: 'Call actions conversation updated', + description: 'Add labels, assign team after conversation updated', + conditions: [ + { + attribute_key: 'company', + filter_operator: 'equal_to', + values: ['Marvel'], + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'customer_type', + filter_operator: 'equal_to', + values: ['platinum'], + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'inbox_id', + filter_operator: 'equal_to', + values: [inbox.id], + query_operator: nil + }.with_indifferent_access + ] + ) + end + + let!(:event) do + Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) + end + + context 'when rule matches with additional_attributes and custom_attributes' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label and assign best agents' do + expect(conversation.labels).to eq([]) + expect(conversation.assignee).to be_nil + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + expect(conversation.assignee).to eq(user_1) + end + + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + + listener.conversation_updated(event) + + conversation.reload + + allow(mailer).to receive(:conversation_transcript) + end + + it 'triggers automation rule send message to the contacts' do + expect(conversation.messages).to be_empty + + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + + listener.conversation_updated(event) + + conversation.reload + + expect(conversation.messages.first.content).to eq('Send this message.') + end + + it 'triggers automation rule to mute conversation' do + expect(conversation).not_to be_muted + + listener.conversation_updated(event) + + conversation.reload + expect(conversation).to be_muted + end + + it 'triggers automation_rule with contact standard attributes' do + automation_rule.update!( + conditions: [ + { + attribute_key: 'email', + filter_operator: 'contains', + values: ['example.com'], + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'customer_type', + filter_operator: 'equal_to', + values: ['platinum'], + query_operator: nil + }.with_indifferent_access + ] + ) + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + end + + context 'when inbox condition does not match' do + let!(:inbox_1) { create(:inbox, account: account) } + let!(:event) do + Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) + end + + before do + automation_rule.update!( + event_name: 'conversation_updated', + name: 'Call actions conversation updated', + description: 'Add labels, assign team after conversation updated', + conditions: [ + { + attribute_key: 'inbox_id', + filter_operator: 'equal_to', + values: [inbox_1.id], + query_operator: nil + }.with_indifferent_access + ] + ) + end + + it 'triggers automation rule would not send message to the contacts' do + expect(conversation.messages.count).to eq(0) + expect(conversation.messages).to be_empty + + listener.conversation_updated(event) + + conversation.reload + + expect(conversation.messages.count).to eq(0) + end + end + end + + describe '#conversation_updated' do + before do + automation_rule.update!( + event_name: 'conversation_updated', + name: 'Call actions conversation updated', + description: 'Add labels, assign team after conversation updated' + ) + end + + let!(:event) do + Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) + end + + context 'when rule matches' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + listener.conversation_updated(event) + conversation.reload + + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + listener.conversation_updated(event) + conversation.reload + + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + end + + it 'triggers automation rule to assign best agents' do + expect(conversation.assignee).to be_nil + listener.conversation_updated(event) + conversation.reload + + expect(conversation.assignee).to eq(user_1) + end + + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.conversation_updated(event) + conversation.reload + + allow(mailer).to receive(:conversation_transcript) + end + + it 'triggers automation rule send email to the team' do + message_delivery = instance_double(ActionMailer::MessageDelivery) + + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation).with( + conversation, team, + 'Please pay attention to this conversation, its from high priority customer' + ).and_return(message_delivery) + allow(message_delivery).to receive(:deliver_now) + + listener.conversation_updated(event) + end + + it 'triggers automation rule send message to the contacts' do + expect(conversation.messages).to be_empty + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.conversation_updated(event) + conversation.reload + + expect(conversation.messages.first.content).to eq('Send this message.') + end + end + + context 'when rule matches based on custom_attributes' do + before do + conversation.update!(custom_attributes: { priority: 'P2' }) + conversation.contact.update!(custom_attributes: { cloud_customer: false }) + + automation_rule.update!( + event_name: 'conversation_updated', + name: 'Priority customer check', + description: 'Add labels, assign team after conversation updated', + conditions: [ + { + attribute_key: 'priority', + filter_operator: 'equal_to', + values: ['P2'], + custom_attribute_type: 'conversation_attribute', + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'cloud_customer', + filter_operator: 'equal_to', + values: [false], + custom_attribute_type: 'contact_attribute', + query_operator: nil + }.with_indifferent_access + ] + ) + end + + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + listener.conversation_updated(event) + conversation.reload + + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + listener.conversation_updated(event) + conversation.reload + + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + end + end + + context 'when conditions based on attribute_changed' do + before do + automation_rule.update!( + event_name: 'conversation_updated', + name: 'Call actions conversation updated when company changed from DC to Marvel', + description: 'Add labels, assign team after conversation updated', + conditions: [ + { + attribute_key: 'company', + filter_operator: 'attribute_changed', + values: { from: ['DC'], to: ['Marvel'] }, + query_operator: 'AND' + }.with_indifferent_access, + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['snoozed'], + query_operator: nil + }.with_indifferent_access + ] + ) + conversation.update(status: :snoozed) + end + + let!(:event) do + Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation, changed_attributes: { + company: %w[DC Marvel] + } }) + end + + context 'when rule matches' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to assign team with OR operator' do + conversation.update(status: :open) + automation_rule.update!( + conditions: [ + { + attribute_key: 'company', + filter_operator: 'attribute_changed', + values: { from: ['DC'], to: ['Marvel'] }, + query_operator: 'OR' + }.with_indifferent_access, + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['snoozed'], + query_operator: nil + }.with_indifferent_access + ] + ) + + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).to eq(team.id) + end + end + + context 'when rule doesnt match' do + it 'when automation rule is triggered it will not assign team' do + conversation.update(status: :open) + + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).not_to eq(team.id) + end + + it 'when automation rule is triggers, it will not assign team on attribute_changed values' do + conversation.update(status: :snoozed) + event = Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation, + changed_attributes: { company: %w[Marvel DC] } }) + + expect(conversation.team_id).not_to eq(team.id) + + listener.conversation_updated(event) + + conversation.reload + expect(conversation.team_id).not_to eq(team.id) + end + end + end + end + + describe '#conversation_opened' do + before do + conversation.update!(status: :open, team_id: team.id) + automation_rule.update!( + event_name: 'conversation_opened', + name: 'Call actions conversation opened', + description: 'Add labels, assign team after conversation updated', + conditions: [{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: nil }.with_indifferent_access] + ) + end + + let!(:event) do + Events::Base.new('conversation_opened', Time.zone.now, { conversation: conversation.reload }) + end + + context 'when rule matches' do + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + listener.conversation_opened(event) + conversation.reload + + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + end + + it 'triggers automation rule to assign best agents' do + expect(conversation.assignee).to be_nil + listener.conversation_opened(event) + conversation.reload + + expect(conversation.assignee).to eq(user_1) + end + + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.conversation_opened(event) + conversation.reload + + allow(mailer).to receive(:conversation_transcript) + end + + it 'triggers automation rule send email to the team' do + message_delivery = instance_double(ActionMailer::MessageDelivery) + + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation).with( + conversation, team, + 'Please pay attention to this conversation, its from high priority customer' + ).and_return(message_delivery) + allow(message_delivery).to receive(:deliver_now) + + listener.conversation_opened(event) + end + + it 'triggers automation rule send message to the contacts' do + expect(conversation.messages).to be_empty + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.conversation_opened(event) + conversation.reload + + expect(conversation.messages.first.content).to eq('Send this message.') + end + end + end + + describe '#message_created event based on case in-sensitive filter' do + before do + automation_rule.update!( + event_name: 'message_created', + name: 'Call actions message created based on case in-sensitive filter', + description: 'Add labels, assign team after message created', + conditions: [ + { 'values': ['KYC'], 'attribute_key': 'content', 'query_operator': nil, 'filter_operator': 'contains' } + ] + ) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming', content: 'KyC message') } + let!(:message_2) { create(:message, account: account, conversation: conversation, message_type: 'incoming', content: 'SALE') } + + let!(:event) do + Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) + end + + it 'triggers automation rule on contains filter' do + expect(conversation.labels).to eq([]) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.message_created(event) + conversation.reload + + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + end + + it 'triggers automation on equal_to filter' do + automation_rule.update!( + conditions: [ + { 'values': ['sale'], 'attribute_key': 'content', 'query_operator': nil, 'filter_operator': 'equal_to' } + ], + actions: [ + { 'action_name' => 'add_label', 'action_params' => %w[sale_enquiry] } + ] + ) + + event = Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message_2 }) + listener.message_created(event) + + conversation.reload + expect(conversation.labels.pluck(:name)).to contain_exactly('sale_enquiry') + end + end + + describe '#message_created' do + before do + automation_rule.update!( + event_name: 'message_created', + name: 'Call actions message created', + description: 'Add labels, assign team after message created', + conditions: [{ 'values': ['incoming'], 'attribute_key': 'message_type', 'query_operator': nil, 'filter_operator': 'equal_to' }] + ) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } + let!(:event) do + Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) + end + + context 'when rule matches' do + it 'triggers automation rule to assign team' do + expect(conversation.team_id).not_to eq(team.id) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.message_created(event) + conversation.reload + + expect(conversation.team_id).to eq(team.id) + end + + it 'triggers automation rule to add label' do + expect(conversation.labels).to eq([]) + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.message_created(event) + conversation.reload + + expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') + end + + it 'triggers automation rule to assign best agent' do + expect(conversation.assignee).to be_nil + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.message_created(event) + conversation.reload + + expect(conversation.assignee).to eq(user_1) + end + + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) + listener.message_created(event) + conversation.reload + + allow(mailer).to receive(:conversation_transcript) + end + end + end + + describe '#message_created with conversation and contacts based conditions' do + before do + automation_rule.update!( + event_name: 'message_created', + name: 'Call actions message created', + description: 'Send Message in the conversation', + conditions: [ + { attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access, + { attribute_key: 'message_type', filter_operator: 'equal_to', values: ['incoming'], query_operator: 'AND' }.with_indifferent_access, + { attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access, + { attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access + ], + actions: [ + { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, + { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } + ] + ) + conversation.update!(team_id: team.id) + conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' }) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } + let!(:event) do + Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) + end + + context 'when rule matches' do + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:conversation_transcript) + + listener.message_created(event) + conversation.reload + + expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com') + end + + it 'triggers automation rule send message to the contacts' do + expect(conversation.messages.count).to eq(1) + listener.message_created(event) + conversation.reload + + expect(conversation.messages.count).to eq(2) + expect(conversation.messages.last.content).to eq('Send this message.') + end + + describe '#conversation_updated with first_reply_created event' do + let(:reporting_event_listener) { ReportingEventListener.instance } + let(:human_message) { create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation) } + let(:first_reply_event) { Events::Base.new('first.reply.created', Time.zone.now, message: human_message) } + + it 'triggers automation rule only once send message to the contacts' do + reporting_event_listener.first_reply_created(first_reply_event) + listener.message_created(event) + conversation.reload + + first_reply = conversation.messages.outgoing.where.not(sender_type: 'AgentBot').where("(additional_attributes->'campaign_id') is null") + + expect(conversation.messages.count).to eq(3) + expect(conversation.messages.last.content).to eq('Send this message.') + expect(first_reply.count).to eq(1) + + automation_message = conversation.messages.last + first_reply_event = Events::Base.new('first.reply.created', Time.zone.now, message: automation_message) + reporting_event_listener.first_reply_created(first_reply_event) + + expect(conversation.messages.count).to eq(3) + expect(first_reply.count).to eq(1) + end + end + end + + context 'when rule does not match' do + before do + conversation.update!(team_id: team.id) + conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' }) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') } + let!(:event) do + Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) + end + + it 'triggers automation rule but wont send message' do + expect(conversation.messages.count).to eq(1) + listener.message_created(event) + conversation.reload + + expect(conversation.messages.count).to eq(1) + expect(conversation.messages.last.content).to eq('Incoming Message') + end + end + end + + describe '#conversation_created with contacts based conditions' do + before do + automation_rule.update!( + event_name: 'conversation_created', + name: 'Call actions message created', + description: 'Send Message in the conversation', + conditions: [ + { attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access, + { attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access, + { attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access + ], + actions: [ + { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, + { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } + ] + ) + conversation.update!(team_id: team.id) + conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' }) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } + let!(:event) do + Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message }) + end + + context 'when rule matches' do + it 'triggers automation rule send email transcript to the mentioned email' do + mailer = double + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:conversation_transcript) + + listener.conversation_created(event) + + conversation.reload + + expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com') + end + + it 'triggers automation rule send message to the contacts' do + expect(conversation.messages.count).to eq(1) + + listener.conversation_created(event) + + conversation.reload + + expect(conversation.messages.count).to eq(2) + expect(conversation.messages.last.content).to eq('Send this message.') + expect(conversation.messages.last.content_attributes[:automation_rule_id]).to eq(automation_rule.id) + end + end + + context 'when rule does not match' do + before do + conversation.update!(team_id: team.id) + conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' }) + end + + let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') } + let!(:event) do + Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message }) + end + + it 'triggers automation rule but wont send message' do + expect(conversation.messages.count).to eq(1) + + listener.conversation_created(event) + + conversation.reload + + expect(conversation.messages.count).to eq(1) + expect(conversation.messages.last.content).to eq('Incoming Message') + end + end + end + + describe '#message_created for tweet events' do + before do + automation_rule.update!( + event_name: 'message_created', + name: 'Call actions message created', + description: 'Send Message in the conversation', + conditions: [ + { attribute_key: 'status', filter_operator: 'equal_to', values: ['open'], query_operator: nil }.with_indifferent_access + ], + actions: [ + { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, + { 'action_name' => 'send_attachment', 'action_params' => [123] }, + { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } + ] + ) + end + + context 'when rule matches' do + let(:tweet) { create(:conversation, additional_attributes: { type: 'tweet' }, inbox: inbox, account: account) } + let(:event) { Events::Base.new('message_created', Time.zone.now, { conversation: tweet, message: message }) } + let!(:message) { create(:message, account: account, conversation: tweet, message_type: 'incoming') } + + it 'triggers automation rule except send_message and send_attachment' do + mailer = double + allow(ConversationReplyMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:conversation_transcript) + + listener.message_created(event) + expect(mailer).to have_received(:conversation_transcript).with(tweet, 'new_agent@example.com') + end + + it 'does not triggers automation rule send message or send attachment' do + expect(tweet.messages.count).to eq(1) + + listener.message_created(event) + + tweet.reload + + expect(tweet.messages.count).to eq(1) + expect(tweet.messages.last.content).to eq(message.content) + end + end + end + + describe '#conversation_created for two accounts' do + let!(:new_account) { create(:account) } + + before do + new_inbox = create(:inbox, account: new_account) + new_conversation = create(:conversation, inbox: new_inbox, account: new_account) + new_automation_rule = create(:automation_rule, account: new_account, name: 'Test Automation Rule - 1') + + create(:message, account: account, conversation: conversation, message_type: 'incoming') + create(:message, account: new_account, conversation: new_conversation, message_type: 'incoming') + + automation_rule.update!( + event_name: 'conversation_created', + conditions: [{ attribute_key: 'status', filter_operator: 'equal_to', values: ['all'], query_operator: nil }.with_indifferent_access], + actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }] + ) + new_automation_rule.update!( + event_name: 'conversation_created', + conditions: [{ attribute_key: 'status', filter_operator: 'equal_to', values: ['all'], query_operator: nil }.with_indifferent_access], + actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message. - 1'] }] + ) + end + + it 'triggers automation at the same time' do + new_conversation = new_account.conversations.last + new_automation_rule = new_account.automation_rules.last + + event = Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation }) + new_event = Events::Base.new('conversation_created', Time.zone.now, { conversation: new_conversation }) + + listener.conversation_created(event) + listener.conversation_created(new_event) + + expect(conversation.messages.count).to eq(2) + expect(new_conversation.messages.count).to eq(2) + + expect(conversation.messages.last.content).to eq('Send this message.') + expect(new_conversation.messages.last.content).to eq('Send this message. - 1') + + expect(conversation.messages.last.content_attributes).to eq({ 'automation_rule_id' => automation_rule.id }) + expect(new_conversation.messages.last.content_attributes).to eq({ 'automation_rule_id' => new_automation_rule.id }) + end + end + + describe '#conversation_created for email inbox' do + let!(:new_account) { create(:account) } + + before do + smtp_email_channel = create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com', + smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: new_account) + email_inbox = smtp_email_channel.inbox + email_conversation = create(:conversation, inbox: email_inbox, account: new_account, + additional_attributes: { 'source' => 'email', 'mail_subject' => 'New conversation test email' }) + new_automation_rule = create(:automation_rule, account: new_account, name: 'Test Automation Rule - 1') + + create(:message, account: new_account, conversation: email_conversation, message_type: 'incoming') + + new_automation_rule.update!( + event_name: 'conversation_created', + conditions: [{ attribute_key: 'mail_subject', filter_operator: 'contains', values: ['test'], query_operator: nil }.with_indifferent_access], + actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message. - 1'] }] + ) + end + + context 'when mail_subject condition matches' do + it 'triggers automation and send message' do + email_conversation = new_account.conversations.last + event = Events::Base.new('conversation_created', Time.zone.now, { conversation: email_conversation }) + + listener.conversation_created(event) + + expect(email_conversation.messages.count).to eq(2) + expect(email_conversation.messages.last.content).to eq('Send this message. - 1') + end + end + + context 'when mail_subject condition does not match' do + it 'does not triggers automation' do + email_conversation = new_account.conversations.last + email_conversation.update!(additional_attributes: { 'source' => 'email', 'mail_subject' => 'New conversation email' }) + event = Events::Base.new('conversation_created', Time.zone.now, { conversation: email_conversation.reload }) + + listener.conversation_created(event) + + expect(email_conversation.messages.count).to eq(1) + expect(email_conversation.messages.last.content).not_to eq('Send this message. - 1') + expect(email_conversation.messages.last.content).to eq('Incoming Message') + end + end + end +end diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index 11e8ee858..46caed67d 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -2,889 +2,166 @@ require 'rails_helper' describe AutomationRuleListener do let(:listener) { described_class.instance } let!(:account) { create(:account) } - let(:inbox) { create(:inbox, account: account) } - let(:contact) { create(:contact, account: account, identifier: '123') } - let(:conversation) { create(:conversation, inbox: inbox, account: account) } - let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') } - let(:team) { create(:team, account: account) } - let(:user_1) { create(:user, role: 0) } - let(:user_2) { create(:user, role: 0) } + let(:conversation) { create(:conversation, account: account) } + let(:conditions_filter_service) { double } + let(:condition_match) { double } + let(:action_service) { double } before do - create(:custom_attribute_definition, - attribute_key: 'customer_type', - account: account, - attribute_model: 'contact_attribute', - attribute_display_type: 'list', - attribute_values: %w[regular platinum gold]) - create(:custom_attribute_definition, - attribute_key: 'priority', - account: account, - attribute_model: 'conversation_attribute', - attribute_display_type: 'list', - attribute_values: %w[P0 P1 P2]) - create(:custom_attribute_definition, - attribute_key: 'cloud_customer', - attribute_display_type: 'checkbox', - account: account, - attribute_model: 'contact_attribute') - create(:team_member, user: user_1, team: team) - create(:team_member, user: user_2, team: team) - create(:inbox_member, user: user_1, inbox: inbox) - create(:inbox_member, user: user_2, inbox: inbox) - create(:account_user, user: user_2, account: account) - create(:account_user, user: user_1, account: account) - - conversation.resolved! - automation_rule.update!(actions: - [ - { - 'action_name' => 'send_email_to_team', 'action_params' => [{ - 'message' => 'Please pay attention to this conversation, its from high priority customer', - 'team_ids' => [team.id] - }] - }, - { 'action_name' => 'assign_team', 'action_params' => [team.id] }, - { 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] }, - { 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] }, - { 'action_name' => 'assign_agent', 'action_params' => [user_1.id] }, - { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }, - { 'action_name' => 'mute_conversation', 'action_params' => nil }, - { 'action_name' => 'change_status', 'action_params' => ['snoozed'] }, - { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, - { 'action_name' => 'send_attachment' } - ]) - file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') - automation_rule.files.attach(file) - automation_rule.save! + allow(AutomationRules::ConditionsFilterService).to receive(:new).and_return(conditions_filter_service) + allow(conditions_filter_service).to receive(:perform).and_return(condition_match) + allow(AutomationRules::ActionService).to receive(:new).and_return(action_service) + allow(action_service).to receive(:perform) end - describe '#conversation_updated with contacts attributes' do - before do - conversation.contact.update!(custom_attributes: { customer_type: 'platinum', signed_in_at: '2022-01-19' }, - additional_attributes: { 'company': 'Marvel' }) - - automation_rule.update!( - event_name: 'conversation_updated', - name: 'Call actions conversation updated', - description: 'Add labels, assign team after conversation updated', - conditions: [ - { - attribute_key: 'company', - filter_operator: 'equal_to', - values: ['Marvel'], - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'customer_type', - filter_operator: 'equal_to', - values: ['platinum'], - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'inbox_id', - filter_operator: 'equal_to', - values: [inbox.id], - query_operator: nil - }.with_indifferent_access - ] - ) + describe 'conversation_created' do + let!(:automation_rule) { create(:automation_rule, event_name: 'conversation_created', account: account) } + let(:event) do + Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, + changed_attributes: { status: %w[nil Open] } }) end - let!(:event) do - Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) - end - - context 'when rule matches with additional_attributes and custom_attributes' do - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).to eq(team.id) - end - - it 'triggers automation rule to add label and assign best agents' do - expect(conversation.labels).to eq([]) - expect(conversation.assignee).to be_nil - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - expect(conversation.assignee).to eq(user_1) - end - - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_updated(event) - - conversation.reload - - allow(mailer).to receive(:conversation_transcript) - end - - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages).to be_empty - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - - listener.conversation_updated(event) - - conversation.reload - - expect(conversation.messages.first.content).to eq('Send this message.') - end - - it 'triggers automation rule to mute conversation' do - expect(conversation).not_to be_muted - - listener.conversation_updated(event) - - conversation.reload - expect(conversation).to be_muted - end - - it 'triggers automation_rule with contact standard attributes' do - automation_rule.update!( - conditions: [ - { - attribute_key: 'email', - filter_operator: 'contains', - values: ['example.com'], - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'customer_type', - filter_operator: 'equal_to', - values: ['platinum'], - query_operator: nil - }.with_indifferent_access - ] - ) - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).to eq(team.id) - end - end - - context 'when inbox condition does not match' do - let!(:inbox_1) { create(:inbox, account: account) } - let!(:event) do - Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) - end - - before do - automation_rule.update!( - event_name: 'conversation_updated', - name: 'Call actions conversation updated', - description: 'Add labels, assign team after conversation updated', - conditions: [ - { - attribute_key: 'inbox_id', - filter_operator: 'equal_to', - values: [inbox_1.id], - query_operator: nil - }.with_indifferent_access - ] - ) - end - - it 'triggers automation rule would not send message to the contacts' do - expect(conversation.messages.count).to eq(0) - expect(conversation.messages).to be_empty - - listener.conversation_updated(event) - - conversation.reload - - expect(conversation.messages.count).to eq(0) - end - end - end - - describe '#conversation_updated' do - before do - automation_rule.update!( - event_name: 'conversation_updated', - name: 'Call actions conversation updated', - description: 'Add labels, assign team after conversation updated' - ) - end - - let!(:event) do - Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation }) - end - - context 'when rule matches' do - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - listener.conversation_updated(event) - conversation.reload - - expect(conversation.team_id).to eq(team.id) - end - - it 'triggers automation rule to add label' do - expect(conversation.labels).to eq([]) - listener.conversation_updated(event) - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - - it 'triggers automation rule to assign best agents' do - expect(conversation.assignee).to be_nil - listener.conversation_updated(event) - conversation.reload - - expect(conversation.assignee).to eq(user_1) - end - - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.conversation_updated(event) - conversation.reload - - allow(mailer).to receive(:conversation_transcript) - end - - it 'triggers automation rule send email to the team' do - message_delivery = instance_double(ActionMailer::MessageDelivery) - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation).with( - conversation, team, - 'Please pay attention to this conversation, its from high priority customer' - ).and_return(message_delivery) - allow(message_delivery).to receive(:deliver_now) - - listener.conversation_updated(event) - end - - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages).to be_empty - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.conversation_updated(event) - conversation.reload - - expect(conversation.messages.first.content).to eq('Send this message.') - end - end - - context 'when rule matches based on custom_attributes' do - before do - conversation.update!(custom_attributes: { priority: 'P2' }) - conversation.contact.update!(custom_attributes: { cloud_customer: false }) - - automation_rule.update!( - event_name: 'conversation_updated', - name: 'Priority customer check', - description: 'Add labels, assign team after conversation updated', - conditions: [ - { - attribute_key: 'priority', - filter_operator: 'equal_to', - values: ['P2'], - custom_attribute_type: 'conversation_attribute', - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'cloud_customer', - filter_operator: 'equal_to', - values: [false], - custom_attribute_type: 'contact_attribute', - query_operator: nil - }.with_indifferent_access - ] - ) - end - - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - listener.conversation_updated(event) - conversation.reload - - expect(conversation.team_id).to eq(team.id) - end - - it 'triggers automation rule to add label' do - expect(conversation.labels).to eq([]) - listener.conversation_updated(event) - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - end - - context 'when conditions based on attribute_changed' do - before do - automation_rule.update!( - event_name: 'conversation_updated', - name: 'Call actions conversation updated when company changed from DC to Marvel', - description: 'Add labels, assign team after conversation updated', - conditions: [ - { - attribute_key: 'company', - filter_operator: 'attribute_changed', - values: { from: ['DC'], to: ['Marvel'] }, - query_operator: 'AND' - }.with_indifferent_access, - { - attribute_key: 'status', - filter_operator: 'equal_to', - values: ['snoozed'], - query_operator: nil - }.with_indifferent_access - ] - ) - conversation.update(status: :snoozed) - end - - let!(:event) do - Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation, changed_attributes: { - company: %w[DC Marvel] - } }) - end - - context 'when rule matches' do - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).to eq(team.id) - end - - it 'triggers automation rule to assign team with OR operator' do - conversation.update(status: :open) - automation_rule.update!( - conditions: [ - { - attribute_key: 'company', - filter_operator: 'attribute_changed', - values: { from: ['DC'], to: ['Marvel'] }, - query_operator: 'OR' - }.with_indifferent_access, - { - attribute_key: 'status', - filter_operator: 'equal_to', - values: ['snoozed'], - query_operator: nil - }.with_indifferent_access - ] - ) - - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).to eq(team.id) - end - end - - context 'when rule doesnt match' do - it 'when automation rule is triggered it will not assign team' do - conversation.update(status: :open) - - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).not_to eq(team.id) - end - - it 'when automation rule is triggers, it will not assign team on attribute_changed values' do - conversation.update(status: :snoozed) - event = Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation, - changed_attributes: { company: %w[Marvel DC] } }) - - expect(conversation.team_id).not_to eq(team.id) - - listener.conversation_updated(event) - - conversation.reload - expect(conversation.team_id).not_to eq(team.id) - end - end - end - end - - describe '#conversation_opened' do - before do - conversation.update!(status: :open, team_id: team.id) - automation_rule.update!( - event_name: 'conversation_opened', - name: 'Call actions conversation opened', - description: 'Add labels, assign team after conversation updated', - conditions: [{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: nil }.with_indifferent_access] - ) - end - - let!(:event) do - Events::Base.new('conversation_opened', Time.zone.now, { conversation: conversation.reload }) - end - - context 'when rule matches' do - it 'triggers automation rule to add label' do - expect(conversation.labels).to eq([]) - listener.conversation_opened(event) - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - - it 'triggers automation rule to assign best agents' do - expect(conversation.assignee).to be_nil - listener.conversation_opened(event) - conversation.reload - - expect(conversation.assignee).to eq(user_1) - end - - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.conversation_opened(event) - conversation.reload - - allow(mailer).to receive(:conversation_transcript) - end - - it 'triggers automation rule send email to the team' do - message_delivery = instance_double(ActionMailer::MessageDelivery) - - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation).with( - conversation, team, - 'Please pay attention to this conversation, its from high priority customer' - ).and_return(message_delivery) - allow(message_delivery).to receive(:deliver_now) - - listener.conversation_opened(event) - end - - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages).to be_empty - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.conversation_opened(event) - conversation.reload - - expect(conversation.messages.first.content).to eq('Send this message.') - end - end - end - - describe '#message_created event based on case in-sensitive filter' do - before do - automation_rule.update!( - event_name: 'message_created', - name: 'Call actions message created based on case in-sensitive filter', - description: 'Add labels, assign team after message created', - conditions: [ - { 'values': ['KYC'], 'attribute_key': 'content', 'query_operator': nil, 'filter_operator': 'contains' } - ] - ) - end - - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming', content: 'KyC message') } - let!(:message_2) { create(:message, account: account, conversation: conversation, message_type: 'incoming', content: 'SALE') } - - let!(:event) do - Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) - end - - it 'triggers automation rule on contains filter' do - expect(conversation.labels).to eq([]) - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.message_created(event) - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - - it 'triggers automation on equal_to filter' do - automation_rule.update!( - conditions: [ - { 'values': ['sale'], 'attribute_key': 'content', 'query_operator': nil, 'filter_operator': 'equal_to' } - ], - actions: [ - { 'action_name' => 'add_label', 'action_params' => %w[sale_enquiry] } - ] - ) - - event = Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message_2 }) - listener.message_created(event) - - conversation.reload - expect(conversation.labels.pluck(:name)).to contain_exactly('sale_enquiry') - end - end - - describe '#message_created' do - before do - automation_rule.update!( - event_name: 'message_created', - name: 'Call actions message created', - description: 'Add labels, assign team after message created', - conditions: [{ 'values': ['incoming'], 'attribute_key': 'message_type', 'query_operator': nil, 'filter_operator': 'equal_to' }] - ) - end - - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } - let!(:event) do - Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) - end - - context 'when rule matches' do - it 'triggers automation rule to assign team' do - expect(conversation.team_id).not_to eq(team.id) - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.message_created(event) - conversation.reload - - expect(conversation.team_id).to eq(team.id) - end - - it 'triggers automation rule to add label' do - expect(conversation.labels).to eq([]) - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.message_created(event) - conversation.reload - - expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer') - end - - it 'triggers automation rule to assign best agent' do - expect(conversation.assignee).to be_nil - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.message_created(event) - conversation.reload - - expect(conversation.assignee).to eq(user_1) - end - - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation) - listener.message_created(event) - conversation.reload - - allow(mailer).to receive(:conversation_transcript) - end - end - end - - describe '#message_created with conversation and contacts based conditions' do - before do - automation_rule.update!( - event_name: 'message_created', - name: 'Call actions message created', - description: 'Send Message in the conversation', - conditions: [ - { attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access, - { attribute_key: 'message_type', filter_operator: 'equal_to', values: ['incoming'], query_operator: 'AND' }.with_indifferent_access, - { attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access, - { attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access - ], - actions: [ - { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, - { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } - ] - ) - conversation.update!(team_id: team.id) - conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' }) - end - - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } - let!(:event) do - Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) - end - - context 'when rule matches' do - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - allow(ConversationReplyMailer).to receive(:with).and_return(mailer) - allow(mailer).to receive(:conversation_transcript) - - listener.message_created(event) - conversation.reload - - expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com') - end - - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages.count).to eq(1) - listener.message_created(event) - conversation.reload - - expect(conversation.messages.count).to eq(2) - expect(conversation.messages.last.content).to eq('Send this message.') - end - - describe '#conversation_updated with first_reply_created event' do - let(:reporting_event_listener) { ReportingEventListener.instance } - let(:human_message) { create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation) } - let(:first_reply_event) { Events::Base.new('first.reply.created', Time.zone.now, message: human_message) } - - it 'triggers automation rule only once send message to the contacts' do - reporting_event_listener.first_reply_created(first_reply_event) - listener.message_created(event) - conversation.reload - - first_reply = conversation.messages.outgoing.where.not(sender_type: 'AgentBot').where("(additional_attributes->'campaign_id') is null") - - expect(conversation.messages.count).to eq(3) - expect(conversation.messages.last.content).to eq('Send this message.') - expect(first_reply.count).to eq(1) - - automation_message = conversation.messages.last - first_reply_event = Events::Base.new('first.reply.created', Time.zone.now, message: automation_message) - reporting_event_listener.first_reply_created(first_reply_event) - - expect(conversation.messages.count).to eq(3) - expect(first_reply.count).to eq(1) - end - end - end - - context 'when rule does not match' do - before do - conversation.update!(team_id: team.id) - conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' }) - end - - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') } - let!(:event) do - Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message }) - end - - it 'triggers automation rule but wont send message' do - expect(conversation.messages.count).to eq(1) - listener.message_created(event) - conversation.reload - - expect(conversation.messages.count).to eq(1) - expect(conversation.messages.last.content).to eq('Incoming Message') - end - end - end - - describe '#conversation_created with contacts based conditions' do - before do - automation_rule.update!( - event_name: 'conversation_created', - name: 'Call actions message created', - description: 'Send Message in the conversation', - conditions: [ - { attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access, - { attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access, - { attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access - ], - actions: [ - { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, - { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } - ] - ) - conversation.update!(team_id: team.id) - conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' }) - end - - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') } - let!(:event) do - Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message }) - end - - context 'when rule matches' do - it 'triggers automation rule send email transcript to the mentioned email' do - mailer = double - allow(ConversationReplyMailer).to receive(:with).and_return(mailer) - allow(mailer).to receive(:conversation_transcript) - + context 'when matching rules are present' do + it 'calls AutomationRules::ActionService if conditions match' do + allow(condition_match).to receive(:present?).and_return(true) listener.conversation_created(event) - - conversation.reload - - expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com') + expect(AutomationRules::ActionService).to have_received(:new).with(automation_rule, account, conversation) end - it 'triggers automation rule send message to the contacts' do - expect(conversation.messages.count).to eq(1) - + it 'does not call AutomationRules::ActionService if conditions do not match' do + allow(condition_match).to receive(:present?).and_return(false) listener.conversation_created(event) - - conversation.reload - - expect(conversation.messages.count).to eq(2) - expect(conversation.messages.last.content).to eq('Send this message.') - expect(conversation.messages.last.content_attributes[:automation_rule_id]).to eq(automation_rule.id) - end - end - - context 'when rule does not match' do - before do - conversation.update!(team_id: team.id) - conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' }) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) end - let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') } - let!(:event) do - Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message }) - end - - it 'triggers automation rule but wont send message' do - expect(conversation.messages.count).to eq(1) - + it 'calls AutomationRules::ActionService for each rule when multiple rules are present' do + create(:automation_rule, event_name: 'conversation_created', account: account) + allow(condition_match).to receive(:present?).and_return(true) listener.conversation_created(event) + expect(AutomationRules::ActionService).to have_received(:new).twice + end - conversation.reload - - expect(conversation.messages.count).to eq(1) - expect(conversation.messages.last.content).to eq('Incoming Message') + it 'does not call AutomationRules::ActionService if performed by automation' do + event.data[:performed_by] = automation_rule + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_created(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) end end end - describe '#message_created for tweet events' do - before do - automation_rule.update!( - event_name: 'message_created', - name: 'Call actions message created', - description: 'Send Message in the conversation', - conditions: [ - { attribute_key: 'status', filter_operator: 'equal_to', values: ['open'], query_operator: nil }.with_indifferent_access - ], - actions: [ - { 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }, - { 'action_name' => 'send_attachment', 'action_params' => [123] }, - { 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] } - ] - ) + describe 'conversation_updated' do + let!(:automation_rule) { create(:automation_rule, event_name: 'conversation_updated', account: account) } + let(:event) do + Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation, + changed_attributes: { status: %w[Resolved Open] } }) end - context 'when rule matches' do - let(:tweet) { create(:conversation, additional_attributes: { type: 'tweet' }, inbox: inbox, account: account) } - let(:event) { Events::Base.new('message_created', Time.zone.now, { conversation: tweet, message: message }) } - let!(:message) { create(:message, account: account, conversation: tweet, message_type: 'incoming') } + context 'when matching rules are present' do + it 'calls AutomationRules::ActionService if conditions match' do + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_updated(event) + expect(AutomationRules::ActionService).to have_received(:new).with(automation_rule, account, conversation) + end - it 'triggers automation rule except send_message and send_attachment' do - mailer = double - allow(ConversationReplyMailer).to receive(:with).and_return(mailer) - allow(mailer).to receive(:conversation_transcript) + it 'does not call AutomationRules::ActionService if conditions do not match' do + allow(condition_match).to receive(:present?).and_return(false) + listener.conversation_updated(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) + end + it 'calls AutomationRules::ActionService for each rule when multiple rules are present' do + create(:automation_rule, event_name: 'conversation_updated', account: account) + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_updated(event) + expect(AutomationRules::ActionService).to have_received(:new).twice + end + + it 'does not call AutomationRules::ActionService if performed by automation' do + event.data[:performed_by] = automation_rule + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_updated(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) + end + end + end + + describe 'conversation_opened' do + let!(:automation_rule) { create(:automation_rule, event_name: 'conversation_opened', account: account) } + let(:event) do + Events::Base.new('conversation_opened', Time.zone.now, { conversation: conversation, + changed_attributes: { status: %w[Resolved Open] } }) + end + + context 'when matching rules are present' do + it 'calls AutomationRules::ActionService if conditions match' do + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_opened(event) + expect(AutomationRules::ActionService).to have_received(:new).with(automation_rule, account, conversation) + end + + it 'does not call AutomationRules::ActionService if conditions do not match' do + allow(condition_match).to receive(:present?).and_return(false) + listener.conversation_opened(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) + end + + it 'calls AutomationRules::ActionService for each rule when multiple rules are present' do + create(:automation_rule, event_name: 'conversation_opened', account: account) + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_opened(event) + expect(AutomationRules::ActionService).to have_received(:new).twice + end + + it 'does not call AutomationRules::ActionService if performed by automation' do + event.data[:performed_by] = automation_rule + allow(condition_match).to receive(:present?).and_return(true) + listener.conversation_opened(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) + end + end + end + + describe 'message_created' do + let!(:automation_rule) { create(:automation_rule, event_name: 'message_created', account: account) } + let!(:message) { create(:message, account: account, conversation: conversation) } + let(:event) do + Events::Base.new('message_created', Time.zone.now, { message: message, + changed_attributes: { content: %w[nil Hi] } }) + end + + context 'when matching rules are present' do + it 'calls AutomationRules::ActionService if conditions match' do + allow(condition_match).to receive(:present?).and_return(true) listener.message_created(event) - expect(mailer).to have_received(:conversation_transcript).with(tweet, 'new_agent@example.com') + expect(AutomationRules::ActionService).to have_received(:new).with(automation_rule, account, conversation) end - it 'does not triggers automation rule send message or send attachment' do - expect(tweet.messages.count).to eq(1) - + it 'does not call AutomationRules::ActionService if conditions do not match' do + allow(condition_match).to receive(:present?).and_return(false) listener.message_created(event) - - tweet.reload - - expect(tweet.messages.count).to eq(1) - expect(tweet.messages.last.content).to eq(message.content) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) end - end - end - describe '#conversation_created for two accounts' do - let!(:new_account) { create(:account) } - - before do - new_inbox = create(:inbox, account: new_account) - new_conversation = create(:conversation, inbox: new_inbox, account: new_account) - new_automation_rule = create(:automation_rule, account: new_account, name: 'Test Automation Rule - 1') - - create(:message, account: account, conversation: conversation, message_type: 'incoming') - create(:message, account: new_account, conversation: new_conversation, message_type: 'incoming') - - automation_rule.update!( - event_name: 'conversation_created', - conditions: [{ attribute_key: 'status', filter_operator: 'equal_to', values: ['all'], query_operator: nil }.with_indifferent_access], - actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] }] - ) - new_automation_rule.update!( - event_name: 'conversation_created', - conditions: [{ attribute_key: 'status', filter_operator: 'equal_to', values: ['all'], query_operator: nil }.with_indifferent_access], - actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message. - 1'] }] - ) - end - - it 'triggers automation at the same time' do - new_conversation = new_account.conversations.last - new_automation_rule = new_account.automation_rules.last - - event = Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation }) - new_event = Events::Base.new('conversation_created', Time.zone.now, { conversation: new_conversation }) - - listener.conversation_created(event) - listener.conversation_created(new_event) - - expect(conversation.messages.count).to eq(2) - expect(new_conversation.messages.count).to eq(2) - - expect(conversation.messages.last.content).to eq('Send this message.') - expect(new_conversation.messages.last.content).to eq('Send this message. - 1') - - expect(conversation.messages.last.content_attributes).to eq({ 'automation_rule_id' => automation_rule.id }) - expect(new_conversation.messages.last.content_attributes).to eq({ 'automation_rule_id' => new_automation_rule.id }) - end - end - - describe '#conversation_created for email inbox' do - let!(:new_account) { create(:account) } - - before do - smtp_email_channel = create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com', - smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: new_account) - email_inbox = smtp_email_channel.inbox - email_conversation = create(:conversation, inbox: email_inbox, account: new_account, - additional_attributes: { 'source' => 'email', 'mail_subject' => 'New conversation test email' }) - new_automation_rule = create(:automation_rule, account: new_account, name: 'Test Automation Rule - 1') - - create(:message, account: new_account, conversation: email_conversation, message_type: 'incoming') - - new_automation_rule.update!( - event_name: 'conversation_created', - conditions: [{ attribute_key: 'mail_subject', filter_operator: 'contains', values: ['test'], query_operator: nil }.with_indifferent_access], - actions: [{ 'action_name' => 'send_message', 'action_params' => ['Send this message. - 1'] }] - ) - end - - context 'when mail_subject condition matches' do - it 'triggers automation and send message' do - email_conversation = new_account.conversations.last - event = Events::Base.new('conversation_created', Time.zone.now, { conversation: email_conversation }) - - listener.conversation_created(event) - - expect(email_conversation.messages.count).to eq(2) - expect(email_conversation.messages.last.content).to eq('Send this message. - 1') + it 'calls AutomationRules::ActionService for each rule when multiple rules are present' do + create(:automation_rule, event_name: 'message_created', account: account) + allow(condition_match).to receive(:present?).and_return(true) + listener.message_created(event) + expect(AutomationRules::ActionService).to have_received(:new).twice end - end - context 'when mail_subject condition does not match' do - it 'does not triggers automation' do - email_conversation = new_account.conversations.last - email_conversation.update!(additional_attributes: { 'source' => 'email', 'mail_subject' => 'New conversation email' }) - event = Events::Base.new('conversation_created', Time.zone.now, { conversation: email_conversation.reload }) + it 'does not call AutomationRules::ActionService if performed by automation' do + event.data[:performed_by] = automation_rule + allow(condition_match).to receive(:present?).and_return(true) + listener.message_created(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) + end - listener.conversation_created(event) - - expect(email_conversation.messages.count).to eq(1) - expect(email_conversation.messages.last.content).not_to eq('Send this message. - 1') - expect(email_conversation.messages.last.content).to eq('Incoming Message') + it 'does not call AutomationRules::ActionService if message is activity message' do + message.update!(message_type: 'activity') + allow(condition_match).to receive(:present?).and_return(true) + listener.message_created(event) + expect(AutomationRules::ActionService).not_to have_received(:new).with(automation_rule, account, conversation) end end end diff --git a/spec/services/action_service_spec.rb b/spec/services/action_service_spec.rb new file mode 100644 index 000000000..490873592 --- /dev/null +++ b/spec/services/action_service_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe ::ActionService do + describe '#resolve_conversation' do + let(:conversation) { create(:conversation) } + let(:action_service) { described_class.new(conversation) } + + it 'resolves the conversation' do + expect(conversation.status).to eq('open') + action_service.resolve_conversation(nil) + expect(conversation.reload.status).to eq('resolved') + end + end + # TODO: Expand this test suite +end diff --git a/spec/services/automation_rules/action_service_spec.rb b/spec/services/automation_rules/action_service_spec.rb new file mode 100644 index 000000000..cb54c36b6 --- /dev/null +++ b/spec/services/automation_rules/action_service_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +RSpec.describe AutomationRules::ActionService do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account) } + let(:conversation) { create(:conversation, account: account) } + let!(:rule) do + create(:automation_rule, account: account, + actions: [ + { action_name: 'send_webhook_event', action_params: ['https://example.com'] }, + { action_name: 'send_message', action_params: { message: 'Hello' } } + ]) + end + + describe '#perform' do + context 'when actions are defined in the rule' do + it 'will call the actions' do + expect(Messages::MessageBuilder).to receive(:new) + expect(WebhookJob).to receive(:perform_later) + described_class.new(rule, account, conversation).perform + end + end + + describe '#perform with send_attachment action' do + let(:message_builder) { double } + + before do + allow(Messages::MessageBuilder).to receive(:new).and_return(message_builder) + rule.actions.delete_if { |a| a['action_name'] == 'send_message' } + rule.files.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + rule.save! + rule.actions << { action_name: 'send_attachment', action_params: [rule.files.first.blob_id] } + end + + it 'will send attachment' do + expect(message_builder).to receive(:perform) + described_class.new(rule, account, conversation).perform + end + + it 'will not send attachment is conversation is a tweet' do + twitter_inbox = create(:inbox, channel: create(:channel_twitter_profile, account: account)) + conversation = create(:conversation, inbox: twitter_inbox, additional_attributes: { type: 'tweet' }) + expect(message_builder).not_to receive(:perform) + described_class.new(rule, account, conversation).perform + end + end + + describe '#perform with send_webhook_event action' do + it 'will send webhook event' do + expect(rule.actions.map { |r| r['action_name'] }).to include('send_webhook_event') + expect(WebhookJob).to receive(:perform_later) + described_class.new(rule, account, conversation).perform + end + end + + describe '#perform with send_message action' do + let(:message_builder) { double } + + before do + allow(Messages::MessageBuilder).to receive(:new).and_return(message_builder) + end + + it 'will send message' do + expect(rule.actions.map { |r| r['action_name'] }).to include('send_message') + expect(message_builder).to receive(:perform) + described_class.new(rule, account, conversation).perform + end + + it 'will not send message if conversation is a tweet' do + expect(rule.actions.map { |r| r['action_name'] }).to include('send_message') + twitter_inbox = create(:inbox, channel: create(:channel_twitter_profile, account: account)) + conversation = create(:conversation, inbox: twitter_inbox, additional_attributes: { type: 'tweet' }) + expect(message_builder).not_to receive(:perform) + described_class.new(rule, account, conversation).perform + end + end + + describe '#perform with send_email_to_team action' do + let!(:team) { create(:team, account: account) } + + before do + rule.actions << { action_name: 'send_email_to_team', action_params: [{ team_ids: [team.id], message: 'Hello' }] } + end + + it 'will send email to team' do + expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation).with(conversation, team, 'Hello').and_call_original + described_class.new(rule, account, conversation).perform + end + end + end +end diff --git a/spec/services/automation_rules/conditions_filter_service_spec.rb b/spec/services/automation_rules/conditions_filter_service_spec.rb new file mode 100644 index 000000000..3e6ff2076 --- /dev/null +++ b/spec/services/automation_rules/conditions_filter_service_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +RSpec.describe AutomationRules::ConditionsFilterService do + let(:account) { create(:account) } + let(:conversation) { create(:conversation, account: account) } + let(:rule) { create(:automation_rule, account: account) } + + describe '#perform' do + before do + rule.conditions = [{ 'values': ['open'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }] + rule.save + end + + context 'when conditions in rule matches with object' do + it 'will return true' do + expect(described_class.new(rule, conversation, { changed_attributes: { status: [nil, 'open'] } }).perform).to be(true) + end + end + + context 'when conditions in rule does not match with object' do + it 'will return false' do + conversation.update(status: 'resolved') + expect(described_class.new(rule, conversation, { changed_attributes: { status: %w[open resolved] } }).perform).to be(false) + end + end + end + + ## TODO: add tests for the other conditions +end