Files
chatwoot/spec/models/campaign_spec.rb
Sony MathewandGitHub 66cfb26c77 feat: Add unread count filters feature flag (1/6) (#14885)
## Description

Adds the account-level `unread_count_for_filters` feature flag as the
dark-launch gate for filtered sidebar unread counts. This reuses the
deprecated `quoted_email_reply` flag slot, resets the reused bit for
existing accounts, and removes stale defaults so new accounts do not
reference the old flag.

This also adds the feature where we are now calculating the unread counts for built in filters like mentions, participating and unattended along with unread count for saved filters/folders.

Closes
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
2026-07-08 23:50:40 +05:30

249 lines
8.6 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Campaign do
let(:store) { Conversations::UnreadCounts::FilteredCountStore }
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:inbox) }
end
describe '#destroy' do
let(:account) { create(:account) }
let(:campaign) { create(:campaign, account: account) }
before do
campaign
allow(Rails.configuration.dispatcher).to receive(:dispatch)
end
after do
Redis::Alfred.delete(store.conversation_version_key(account.id))
end
it 'invalidates and refreshes filtered counts when conversations are detached from a deleted campaign' do
account.enable_features!(:unread_count_for_filters)
expect do
campaign.destroy!
end.to change { store.conversation_version(account.id) }.by(1)
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'account.cache_invalidated',
kind_of(Time),
account: account,
cache_keys: account.cache_keys
)
end
it 'does not notify filtered count refreshes when the feature is disabled' do
campaign.destroy!
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
end
end
describe '.before_create' do
let(:account) { create(:account) }
let(:website_channel) { create(:channel_widget, account: account) }
let(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
let(:campaign) { build(:campaign, account: account, inbox: website_inbox, display_id: nil, trigger_rules: { url: 'https://test.com' }) }
before do
campaign.save!
campaign.reload
end
it 'runs before_create callbacks' do
expect(campaign.display_id).to eq(1)
end
end
context 'when Inbox other then Website or Twilio SMS' do
before do
stub_request(:post, /graph.facebook.com/)
end
let(:account) { create(:account) }
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
let(:campaign) { build(:campaign, inbox: facebook_inbox, account: account) }
it 'would not save the campaigns' do
expect(campaign.save).to be false
expect(campaign.errors.full_messages.first).to eq 'Inbox Unsupported Inbox type'
end
end
context 'when a campaign is completed' do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let!(:campaign) { create(:campaign, account: account, inbox: web_widget.inbox, campaign_status: :completed, trigger_rules: { url: 'https://test.com' }) }
it 'would prevent further updates' do
campaign.title = 'new name'
expect(campaign.save).to be false
expect(campaign.errors.full_messages.first).to eq 'Status The campaign is already completed'
end
it 'can be deleted' do
campaign.destroy!
expect(described_class.exists?(campaign.id)).to be false
end
it 'cant be triggered' do
expect(Twilio::OneoffSmsCampaignService).not_to receive(:new).with(campaign: campaign)
expect(campaign.trigger!).to be_nil
end
end
describe 'ensure_correct_campaign_attributes' do
context 'when Twilio SMS campaign' do
let(:account) { create(:account) }
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
let(:campaign) { build(:campaign, account: account, inbox: twilio_inbox) }
it 'only saves campaign type as oneoff and wont leave scheduled_at empty' do
campaign.campaign_type = 'ongoing'
campaign.save!
expect(campaign.reload.campaign_type).to eq 'one_off'
expect(campaign.scheduled_at.present?).to be true
end
it 'calls twilio service on trigger!' do
sms_service = double
expect(Twilio::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
expect(sms_service).to receive(:perform)
campaign.save!
campaign.trigger!
end
it 'marks the campaign as processing before triggering the service' do
campaign.save!
sms_service = double
expect(Twilio::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
expect(sms_service).to receive(:perform) do
expect(campaign.reload.processing?).to be true
end
campaign.trigger!
end
it 'does not trigger a processing campaign again' do
campaign.save!
campaign.processing!
expect(Twilio::OneoffSmsCampaignService).not_to receive(:new)
campaign.trigger!
end
it 'keeps the campaign processing when triggering fails' do
campaign.save!
sms_service = double
expect(Twilio::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
expect(sms_service).to receive(:perform).and_raise(StandardError, 'provider error')
expect { campaign.trigger! }.to raise_error(StandardError, 'provider error')
expect(campaign.reload.processing?).to be true
end
end
context 'when SMS campaign' do
let(:account) { create(:account) }
let!(:sms_channel) { create(:channel_sms, account: account) }
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
let(:campaign) { build(:campaign, account: account, inbox: sms_inbox) }
it 'only saves campaign type as oneoff and wont leave scheduled_at empty' do
campaign.campaign_type = 'ongoing'
campaign.save!
expect(campaign.reload.campaign_type).to eq 'one_off'
expect(campaign.scheduled_at.present?).to be true
end
it 'calls sms service on trigger!' do
sms_service = double
expect(Sms::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
expect(sms_service).to receive(:perform)
campaign.save!
campaign.trigger!
end
end
context 'when WhatsApp campaign feature is disabled' do
let(:account) { create(:account) }
let(:whatsapp_channel) do
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false)
end
let(:campaign) { create(:campaign, account: account, inbox: whatsapp_channel.inbox) }
it 'does not mark the campaign as processing' do
expect(Whatsapp::OneoffCampaignService).not_to receive(:new)
campaign.trigger!
expect(campaign.reload.active?).to be true
end
end
context 'when Website campaign' do
let(:campaign) { build(:campaign) }
it 'only saves campaign type as ongoing' do
campaign.campaign_type = 'one_off'
campaign.save!
expect(campaign.reload.campaign_type).to eq 'ongoing'
end
end
end
context 'when validating sender' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:inbox) { create(:inbox, channel: web_widget, account: account) }
it 'allows sender from the same account' do
campaign = build(:campaign, inbox: inbox, account: account, sender: user)
expect(campaign).to be_valid
end
it 'does not allow sender from different account' do
other_account = create(:account)
other_user = create(:user, account: other_account)
campaign = build(:campaign, inbox: inbox, account: account, sender: other_user)
expect(campaign).not_to be_valid
expect(campaign.errors[:sender_id]).to include(
'must belong to the same account as the campaign'
)
end
end
context 'when validating inbox' do
let(:account) { create(:account) }
let(:other_account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:inbox) { create(:inbox, channel: web_widget, account: account) }
let(:other_account_inbox) { create(:inbox, account: other_account) }
it 'allows inbox from the same account' do
campaign = build(:campaign, inbox: inbox, account: account)
expect(campaign).to be_valid
end
it 'does not allow inbox from different account' do
campaign = build(:campaign, inbox: other_account_inbox, account: account)
expect(campaign).not_to be_valid
expect(campaign.errors[:inbox_id]).to include(
'must belong to the same account as the campaign'
)
end
end
end