feat: Add support for labels in automations (#11658)
- Add support for using labels as an action event for automation - Fix duplicated conversation_updated event dispatch for labels Fixes https://github.com/chatwoot/chatwoot/issues/8539 and multiple issues around duplication related to label change events. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
244
spec/listeners/automation_rule_listener_labels_spec.rb
Normal file
244
spec/listeners/automation_rule_listener_labels_spec.rb
Normal file
@@ -0,0 +1,244 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe AutomationRuleListener do
|
||||
let(:listener) { described_class.instance }
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let(:label1) { create(:label, account: account, title: 'bug') }
|
||||
let(:label2) { create(:label, account: account, title: 'feature') }
|
||||
let(:label3) { create(:label, account: account, title: 'urgent') }
|
||||
|
||||
before do
|
||||
Current.user = user
|
||||
end
|
||||
|
||||
describe 'conversation_updated with label conditions and actions' do
|
||||
context 'when label is added and automation rule has label condition' do
|
||||
let(:automation_rule) do
|
||||
create(:automation_rule,
|
||||
event_name: 'conversation_updated',
|
||||
account: account,
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['bug'],
|
||||
query_operator: nil
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: ['urgent']
|
||||
},
|
||||
{
|
||||
action_name: 'send_message',
|
||||
action_params: ['Bug report received. We will investigate this issue.']
|
||||
}
|
||||
])
|
||||
end
|
||||
|
||||
it 'triggers automation when the specified label is added' do
|
||||
automation_rule # Create the automation rule
|
||||
expect(Messages::MessageBuilder).to receive(:new).and_call_original
|
||||
|
||||
# Add the 'bug' label to trigger the automation
|
||||
conversation.add_labels(['bug'])
|
||||
|
||||
# Dispatch the event
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [[], ['bug']] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
# Verify the label was added by automation
|
||||
expect(conversation.reload.label_list).to include('urgent')
|
||||
|
||||
# Verify a message was sent
|
||||
expect(conversation.messages.last.content).to eq('Bug report received. We will investigate this issue.')
|
||||
end
|
||||
|
||||
it 'does not trigger automation when a different label is added' do
|
||||
automation_rule # Create the automation rule
|
||||
expect(Messages::MessageBuilder).not_to receive(:new)
|
||||
|
||||
# Add a different label
|
||||
conversation.add_labels(['feature'])
|
||||
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [[], ['feature']] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
# Verify the automation did not run
|
||||
expect(conversation.reload.label_list).not_to include('urgent')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when automation rule has is_present label condition' do
|
||||
let(:automation_rule) do
|
||||
create(:automation_rule,
|
||||
event_name: 'conversation_updated',
|
||||
account: account,
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'is_present',
|
||||
values: [],
|
||||
query_operator: nil
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'send_message',
|
||||
action_params: ['Thank you for adding a label to categorize this conversation.']
|
||||
}
|
||||
])
|
||||
end
|
||||
|
||||
it 'triggers automation when any label is added to an unlabeled conversation' do
|
||||
automation_rule # Create the automation rule
|
||||
expect(Messages::MessageBuilder).to receive(:new).and_call_original
|
||||
|
||||
# Add any label to trigger the automation
|
||||
conversation.add_labels(['feature'])
|
||||
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [[], ['feature']] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
# Verify a message was sent
|
||||
expect(conversation.messages.last.content).to eq('Thank you for adding a label to categorize this conversation.')
|
||||
end
|
||||
|
||||
it 'still triggers when labels are removed but conversation still has labels' do
|
||||
automation_rule # Create the automation rule
|
||||
# Start with multiple labels
|
||||
conversation.add_labels(%w[bug feature])
|
||||
conversation.reload
|
||||
|
||||
expect(Messages::MessageBuilder).to receive(:new).and_call_original
|
||||
|
||||
# Remove one label but conversation still has labels
|
||||
conversation.update_labels(['bug'])
|
||||
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [%w[bug feature], ['bug']] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
# Should still trigger because conversation has labels (is_present condition)
|
||||
expect(conversation.messages.last.content).to eq('Thank you for adding a label to categorize this conversation.')
|
||||
end
|
||||
|
||||
it 'does not trigger when all labels are removed' do
|
||||
automation_rule # Create the automation rule
|
||||
# Start with labels
|
||||
conversation.add_labels(['bug'])
|
||||
conversation.reload
|
||||
|
||||
expect(Messages::MessageBuilder).not_to receive(:new)
|
||||
|
||||
# Remove all labels
|
||||
conversation.update_labels([])
|
||||
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [['bug'], []] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when automation rule has remove_label action' do
|
||||
let!(:automation_rule) do
|
||||
create(:automation_rule,
|
||||
event_name: 'conversation_updated',
|
||||
account: account,
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['urgent'],
|
||||
query_operator: nil
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'remove_label',
|
||||
action_params: ['bug']
|
||||
}
|
||||
])
|
||||
end
|
||||
|
||||
it 'removes specified labels when condition is met' do
|
||||
automation_rule # Create the automation rule
|
||||
# Start with both labels
|
||||
conversation.add_labels(%w[bug urgent])
|
||||
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [['bug'], %w[bug urgent]] }
|
||||
})
|
||||
|
||||
listener.conversation_updated(event)
|
||||
|
||||
# Verify the bug label was removed but urgent remains
|
||||
expect(conversation.reload.label_list).to include('urgent')
|
||||
expect(conversation.reload.label_list).not_to include('bug')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'preventing infinite loops' do
|
||||
let!(:automation_rule) do
|
||||
create(:automation_rule,
|
||||
event_name: 'conversation_updated',
|
||||
account: account,
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['bug'],
|
||||
query_operator: nil
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: ['processed']
|
||||
}
|
||||
])
|
||||
end
|
||||
|
||||
it 'does not trigger automation when performed by automation rule' do
|
||||
automation_rule # Create the automation rule
|
||||
conversation.add_labels(['bug'])
|
||||
|
||||
# Simulate event performed by automation rule
|
||||
event = Events::Base.new('conversation_updated', Time.zone.now, {
|
||||
conversation: conversation,
|
||||
changed_attributes: { label_list: [[], ['bug']] },
|
||||
performed_by: automation_rule
|
||||
})
|
||||
|
||||
# Should not process the event since it was performed by automation
|
||||
expect(AutomationRules::ActionService).not_to receive(:new)
|
||||
|
||||
listener.conversation_updated(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -60,6 +60,32 @@ RSpec.describe AutomationRule do
|
||||
expect(rule.valid?).to be false
|
||||
expect(rule.errors.messages[:conditions]).to eq(['Automation conditions should have query operator.'])
|
||||
end
|
||||
|
||||
it 'allows labels as a valid condition attribute' do
|
||||
params[:conditions] = [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['bug'],
|
||||
query_operator: nil
|
||||
}
|
||||
]
|
||||
rule = FactoryBot.build(:automation_rule, params)
|
||||
expect(rule.valid?).to be true
|
||||
end
|
||||
|
||||
it 'validates label condition operators' do
|
||||
params[:conditions] = [
|
||||
{
|
||||
attribute_key: 'labels',
|
||||
filter_operator: 'is_present',
|
||||
values: [],
|
||||
query_operator: nil
|
||||
}
|
||||
]
|
||||
rule = FactoryBot.build(:automation_rule, params)
|
||||
expect(rule.valid?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'reauthorizable' do
|
||||
|
||||
@@ -136,7 +136,7 @@ RSpec.describe Conversation do
|
||||
notifiable_assignee_change: false,
|
||||
changed_attributes: changed_attributes,
|
||||
performed_by: nil
|
||||
).exactly(2).times
|
||||
)
|
||||
end
|
||||
|
||||
it 'runs after_update callbacks' do
|
||||
|
||||
@@ -118,6 +118,45 @@ RSpec.describe AutomationRules::ActionService do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform with add_label action' do
|
||||
before do
|
||||
rule.actions << { action_name: 'add_label', action_params: %w[bug feature] }
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will add labels to conversation' do
|
||||
described_class.new(rule, account, conversation).perform
|
||||
expect(conversation.reload.label_list).to include('bug', 'feature')
|
||||
end
|
||||
|
||||
it 'will not duplicate existing labels' do
|
||||
conversation.add_labels(['bug'])
|
||||
described_class.new(rule, account, conversation).perform
|
||||
expect(conversation.reload.label_list.count('bug')).to eq(1)
|
||||
expect(conversation.reload.label_list).to include('feature')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform with remove_label action' do
|
||||
before do
|
||||
conversation.add_labels(%w[bug feature support])
|
||||
rule.actions << { action_name: 'remove_label', action_params: %w[bug feature] }
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will remove specified labels from conversation' do
|
||||
described_class.new(rule, account, conversation).perform
|
||||
expect(conversation.reload.label_list).not_to include('bug', 'feature')
|
||||
expect(conversation.reload.label_list).to include('support')
|
||||
end
|
||||
|
||||
it 'will not fail if labels do not exist on conversation' do
|
||||
conversation.update_labels(['support']) # Remove bug and feature first
|
||||
expect { described_class.new(rule, account, conversation).perform }.not_to raise_error
|
||||
expect(conversation.reload.label_list).to include('support')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform with add_private_note action' do
|
||||
let(:message_builder) { double }
|
||||
|
||||
|
||||
@@ -134,5 +134,86 @@ RSpec.describe AutomationRules::ConditionsFilterService do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conditions based on labels' do
|
||||
before do
|
||||
conversation.add_labels(['bug'])
|
||||
end
|
||||
|
||||
context 'when filter_operator is equal_to' do
|
||||
before do
|
||||
rule.conditions = [
|
||||
{ 'values': ['bug'], 'attribute_key': 'labels', 'query_operator': nil, 'filter_operator': 'equal_to' }
|
||||
]
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will return true when conversation has the label' do
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(true)
|
||||
end
|
||||
|
||||
it 'will return false when conversation does not have the label' do
|
||||
rule.conditions = [
|
||||
{ 'values': ['feature'], 'attribute_key': 'labels', 'query_operator': nil, 'filter_operator': 'equal_to' }
|
||||
]
|
||||
rule.save
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filter_operator is not_equal_to' do
|
||||
before do
|
||||
rule.conditions = [
|
||||
{ 'values': ['feature'], 'attribute_key': 'labels', 'query_operator': nil, 'filter_operator': 'not_equal_to' }
|
||||
]
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will return true when conversation does not have the label' do
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(true)
|
||||
end
|
||||
|
||||
it 'will return false when conversation has the label' do
|
||||
conversation.add_labels(['feature'])
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filter_operator is is_present' do
|
||||
before do
|
||||
rule.conditions = [
|
||||
{ 'values': [], 'attribute_key': 'labels', 'query_operator': nil, 'filter_operator': 'is_present' }
|
||||
]
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will return true when conversation has any labels' do
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(true)
|
||||
end
|
||||
|
||||
it 'will return false when conversation has no labels' do
|
||||
conversation.update_labels([])
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filter_operator is is_not_present' do
|
||||
before do
|
||||
rule.conditions = [
|
||||
{ 'values': [], 'attribute_key': 'labels', 'query_operator': nil, 'filter_operator': 'is_not_present' }
|
||||
]
|
||||
rule.save
|
||||
end
|
||||
|
||||
it 'will return false when conversation has any labels' do
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(false)
|
||||
end
|
||||
|
||||
it 'will return true when conversation has no labels' do
|
||||
conversation.update_labels([])
|
||||
expect(described_class.new(rule, conversation, { changed_attributes: {} }).perform).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user