Merge branch 'develop' into feat/whatsapp-embedded-signup
This commit is contained in:
@@ -71,6 +71,7 @@ class OauthCallbackController < ApplicationController
|
||||
def create_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_email,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@@ -7,7 +8,21 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'InterDisplay', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
font-family:
|
||||
'InterDisplay',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif,
|
||||
'Noto Sans',
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Noto Color Emoji';
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
}
|
||||
if (el.tag === 'h2') {
|
||||
if (this.h1Count > 0) {
|
||||
return 'ml-2';
|
||||
return 'ltr:ml-2 rtl:mr-2';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
if (!this.h1Count && !this.h2Count) {
|
||||
return '';
|
||||
}
|
||||
return 'ml-5';
|
||||
return 'ltr:ml-5 rtl:mr-5';
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -94,17 +94,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hidden lg:block flex-1 py-6 scroll-mt-24 pl-4 sticky top-24">
|
||||
<div
|
||||
class="hidden lg:block flex-1 py-6 scroll-mt-24 ltr:pl-4 rtl:pr-4 sticky top-24"
|
||||
>
|
||||
<div v-if="rows.length > 0" class="py-2 overflow-auto">
|
||||
<nav class="max-w-2xl">
|
||||
<ol
|
||||
role="list"
|
||||
class="flex flex-col gap-2 text-base border-l-2 border-solid border-slate-100 dark:border-slate-800"
|
||||
class="flex flex-col gap-2 text-base ltr:border-l-2 rtl:border-r-2 border-solid border-slate-100 dark:border-slate-800"
|
||||
>
|
||||
<li
|
||||
v-for="element in rows"
|
||||
:key="element.slug"
|
||||
class="leading-6 border-l-2 relative -left-0.5 border-solid"
|
||||
class="leading-6 ltr:border-l-2 rtl:border-r-2 relative ltr:-left-0.5 rtl:-right-0.5 border-solid"
|
||||
:class="elementBorderStyles(element)"
|
||||
>
|
||||
<p class="py-1 px-3" :class="getClassName(element)">
|
||||
|
||||
@@ -8,6 +8,7 @@ import slugifyWithCounter from '@sindresorhus/slugify';
|
||||
import PublicArticleSearch from './components/PublicArticleSearch.vue';
|
||||
import TableOfContents from './components/TableOfContents.vue';
|
||||
import { initializeTheme } from './portalThemeHelper.js';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
|
||||
export const getHeadingsfromTheArticle = () => {
|
||||
const rows = [];
|
||||
@@ -114,10 +115,19 @@ export const InitializationHelpers = {
|
||||
});
|
||||
},
|
||||
|
||||
setDirectionAttribute: () => {
|
||||
const portalElement = document.getElementById('portal');
|
||||
if (!portalElement) return;
|
||||
|
||||
const locale = document.querySelector('.locale-switcher')?.value;
|
||||
portalElement.dir = locale && getLanguageDirection(locale) ? 'rtl' : 'ltr';
|
||||
},
|
||||
|
||||
initializeThemesInPortal: initializeTheme,
|
||||
|
||||
initialize: () => {
|
||||
openExternalLinksInNewTab();
|
||||
InitializationHelpers.setDirectionAttribute();
|
||||
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
|
||||
InitializationHelpers.appendPlainParamToURLs();
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,8 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
||||
|
||||
@agent = agent
|
||||
@conversation = conversation
|
||||
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}."
|
||||
inbox_name = @conversation.inbox&.sanitized_name
|
||||
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{inbox_name}."
|
||||
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
|
||||
send_mail_with_liquid(to: @agent.email, subject: subject) and return
|
||||
end
|
||||
|
||||
@@ -104,7 +104,7 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def business_name
|
||||
@inbox.business_name || @inbox.name
|
||||
@inbox.business_name || @inbox.sanitized_name
|
||||
end
|
||||
|
||||
def from_email
|
||||
|
||||
+26
-2
@@ -47,8 +47,6 @@ class Inbox < ApplicationRecord
|
||||
|
||||
# Not allowing characters:
|
||||
validates :name, presence: true
|
||||
validates :name, if: :check_channel_type?, format: { with: %r{^^\b[^/\\<>@]*\b$}, multiline: true,
|
||||
message: I18n.t('errors.inboxes.validations.name') }
|
||||
validates :account_id, presence: true
|
||||
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
||||
validates :out_of_office_message, length: { maximum: Limits::OUT_OF_OFFICE_MESSAGE_MAX_LENGTH }
|
||||
@@ -99,6 +97,16 @@ class Inbox < ApplicationRecord
|
||||
update_account_cache
|
||||
end
|
||||
|
||||
# Sanitizes inbox name for balanced email provider compatibility
|
||||
# ALLOWS: /'._- and Unicode letters/numbers/emojis
|
||||
# REMOVES: Forbidden chars (\<>@") + spam-trigger symbols (!#$%&*+=?^`{|}~)
|
||||
def sanitized_name
|
||||
return default_name_for_blank_name if name.blank?
|
||||
|
||||
sanitized = apply_sanitization_rules(name)
|
||||
sanitized.blank? && email? ? display_name_from_email : sanitized
|
||||
end
|
||||
|
||||
def sms?
|
||||
channel_type == 'Channel::Sms'
|
||||
end
|
||||
@@ -178,6 +186,22 @@ class Inbox < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def default_name_for_blank_name
|
||||
email? ? display_name_from_email : ''
|
||||
end
|
||||
|
||||
def apply_sanitization_rules(name)
|
||||
name.gsub(/[\\<>@"!#$%&*+=?^`{|}~]/, '') # Remove forbidden chars
|
||||
.gsub(/[\x00-\x1F\x7F]/, ' ') # Replace control chars with spaces
|
||||
.gsub(/\A[[:punct:]]+|[[:punct:]]+\z/, '') # Remove leading/trailing punctuation
|
||||
.gsub(/\s+/, ' ') # Normalize spaces
|
||||
.strip
|
||||
end
|
||||
|
||||
def display_name_from_email
|
||||
channel.email.split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
def dispatch_create_event
|
||||
return if ENV['ENABLE_INBOX_EVENTS'].blank?
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<% author_count = category.articles.published.order(position: :asc).map(&:author).uniq.size %>
|
||||
<% if author_count > 0 %>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<div class="flex flex-row items-center -space-x-2">
|
||||
<% category.articles.published.order(position: :asc).map(&:author).uniq.take(3).each do |author| %>
|
||||
<%= render "public/api/v1/portals/thumbnail", author: author, size: 5 %>
|
||||
<% end %>
|
||||
<div class="flex items-center ltr:flex-row rtl:flex-row-reverse -space-x-2">
|
||||
<% category.articles.published.order(position: :asc).map(&:author).uniq.take(3).each do |author| %>
|
||||
<%= render "public/api/v1/portals/thumbnail", author: author, size: 5 %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% first_author = category.articles.published.order(position: :asc).map(&:author).uniq.first&.name || '' %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex items-center w-full py-5 overflow-hidden">
|
||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10 text-lg font-semibold text-slate-900 dark:text-white">
|
||||
<% if @portal.logo.present? %>
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 mr-2" />
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
||||
<% end %>
|
||||
<%= @portal.name %>
|
||||
</a>
|
||||
@@ -12,7 +12,7 @@
|
||||
<%# Go to homepage link section %>
|
||||
<div class="flex items-center justify-between gap-2 sm:gap-5">
|
||||
<% if @portal.homepage_link %>
|
||||
<div class="hidden px-1 py-2 ml-8 cursor-pointer border-l-1 border-slate-50 dark:border-slate-800 md:block">
|
||||
<div class="hidden px-1 py-2 ltr:ml-8 rtl:mr-8 cursor-pointer md:block">
|
||||
<div class="flex-grow flex-shrink-0">
|
||||
<a id="header-action-button" target="_blank" rel="noopener noreferrer nofollow" href="<%= @portal.homepage_link %>" class="flex flex-row items-center gap-1 text-sm font-medium whitespace-nowrap text-slate-800 dark:text-slate-100 stroke-slate-700 dark:stroke-slate-200">
|
||||
<%= render partial: 'icons/redirect' %>
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<%# Appearance dropdown section %>
|
||||
<div id="appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="absolute flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 right-1 border-slate-100 dark:border-slate-800" aria-hidden="true" style="display: none;" data-dropdown="appearance-dropdown">
|
||||
<div id="appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="absolute flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 ltr:right-1 rtl:left-1 border-slate-100 dark:border-slate-800" aria-hidden="true" style="display: none;" data-dropdown="appearance-dropdown">
|
||||
<button id="toggle-theme-button" data-theme="system" class="flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<%= render partial: 'icons/monitor' %>
|
||||
|
||||
@@ -18,7 +18,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
|
||||
.display_id}] has been created in #{conversation.inbox&.name}.")
|
||||
.display_id}] has been created in #{conversation.inbox&.sanitized_name}.")
|
||||
end
|
||||
|
||||
it 'renders the receiver email' do
|
||||
|
||||
@@ -87,7 +87,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
let(:mail) { described_class.reply_with_summary(message.conversation, message.id).deliver_now }
|
||||
|
||||
it 'has correct name' do
|
||||
expect(mail[:from].display_names).to eq(["#{message.sender.available_name} from Inbox"])
|
||||
expect(mail[:from].display_names).to eq(["#{message.sender.available_name} from #{message.conversation.inbox.sanitized_name}"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -224,11 +224,11 @@ RSpec.describe ConversationReplyMailer do
|
||||
end
|
||||
|
||||
context 'when smtp enabled for email channel' do
|
||||
let(:smtp_email_channel) do
|
||||
let(:smtp_channel) do
|
||||
create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_login: 'smtp@gmail.com',
|
||||
smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account)
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -240,19 +240,19 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
it 'renders sender name in the from address' do
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name even when assignee is not present' do
|
||||
conversation.update(assignee_id: nil)
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{message.sender.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders assignee name in the from address when sender_name not available' do
|
||||
message.update(sender_id: nil)
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.assignee.available_name} from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders inbox name as sender and assignee or business_name not present' do
|
||||
@@ -260,7 +260,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: nil)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "Notifications from #{smtp_email_channel.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "Notifications from #{smtp_channel.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
context 'when friendly name enabled' do
|
||||
@@ -276,7 +276,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "Notifications from #{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as sender and assignee nil and business_name present' do
|
||||
@@ -286,7 +286,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq(
|
||||
"Notifications from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
"Notifications from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -295,7 +295,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: agent.id)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{agent.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as sender and assignee and business_name present' do
|
||||
@@ -304,7 +304,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
conversation.update(assignee_id: agent.id)
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{agent_2.available_name} from #{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -321,7 +321,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.sanitized_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
|
||||
it 'renders sender name as business_name present' do
|
||||
@@ -330,17 +330,17 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
mail = described_class.email_reply(message)
|
||||
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_email_channel.email}>"
|
||||
expect(mail['from'].value).to eq "#{conversation.inbox.business_name} <#{smtp_channel.email}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when smtp enabled for microsoft email channel' do
|
||||
let(:ms_smtp_email_channel) do
|
||||
let(:ms_smtp_channel) do
|
||||
create(:channel_email, imap_login: 'smtp@outlook.com',
|
||||
imap_enabled: true, account: account, provider: 'microsoft', provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -352,11 +352,11 @@ RSpec.describe ConversationReplyMailer do
|
||||
end
|
||||
|
||||
context 'when smtp enabled for google email channel' do
|
||||
let(:ms_smtp_email_channel) do
|
||||
let(:ms_smtp_channel) do
|
||||
create(:channel_email, imap_login: 'smtp@gmail.com',
|
||||
imap_enabled: true, account: account, provider: 'google', provider_config: { access_token: 'access_token' })
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_email_channel.inbox, account: account).reload }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: ms_smtp_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
@@ -430,7 +430,7 @@ RSpec.describe ConversationReplyMailer do
|
||||
|
||||
it 'sets reply to email to be based on the domain' do
|
||||
reply_to_email = "reply+#{message.conversation.uuid}@#{conversation.account.domain}"
|
||||
reply_to = "#{message.sender.available_name} from #{conversation.inbox.name} <#{reply_to_email}>"
|
||||
reply_to = "#{message.sender.available_name} from #{conversation.inbox.sanitized_name} <#{reply_to_email}>"
|
||||
expect(mail['REPLY-TO'].value).to eq(reply_to)
|
||||
expect(mail.reply_to).to eq([reply_to_email])
|
||||
end
|
||||
|
||||
+131
-25
@@ -164,31 +164,7 @@ RSpec.describe Inbox do
|
||||
let(:inbox) { FactoryBot.create(:inbox) }
|
||||
|
||||
context 'when validating inbox name' do
|
||||
it 'does not allow any special character at the end' do
|
||||
inbox.name = 'this is my inbox name-'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow any special character at the start' do
|
||||
inbox.name = '-this is my inbox name'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow chacters like /\@<> in the entire string' do
|
||||
inbox.name = 'inbox@name'
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages).to eq(
|
||||
['Name should not start or end with symbols, and it should not have < > / \\ @ characters.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not empty string' do
|
||||
it 'does not allow empty string' do
|
||||
inbox.name = ''
|
||||
expect(inbox).not_to be_valid
|
||||
expect(inbox.errors.full_messages[0]).to eq(
|
||||
@@ -282,4 +258,134 @@ RSpec.describe Inbox do
|
||||
inbox.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sanitized_name' do
|
||||
context 'when inbox name contains forbidden characters' do
|
||||
it 'removes forbidden and spam-trigger characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test/Name\\With<Bad>@Characters"And\'Quotes!#$%')
|
||||
expect(inbox.sanitized_name).to eq('Test/NameWithBadCharactersAnd\'Quotes')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has leading/trailing non-word characters' do
|
||||
it 'removes leading and trailing symbols' do
|
||||
inbox = FactoryBot.build(:inbox, name: '!!!Test Name***')
|
||||
expect(inbox.sanitized_name).to eq('Test Name')
|
||||
end
|
||||
|
||||
it 'handles mixed leading/trailing characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: '###@@@Test Inbox Name$$$%%')
|
||||
expect(inbox.sanitized_name).to eq('Test Inbox Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has multiple spaces' do
|
||||
it 'normalizes multiple spaces to single space' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test Multiple Spaces')
|
||||
expect(inbox.sanitized_name).to eq('Test Multiple Spaces')
|
||||
end
|
||||
|
||||
it 'handles tabs and other whitespace' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Test\t\nMultiple\r\nSpaces")
|
||||
expect(inbox.sanitized_name).to eq('Test Multiple Spaces')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name has leading/trailing whitespace' do
|
||||
it 'strips whitespace' do
|
||||
inbox = FactoryBot.build(:inbox, name: ' Test Name ')
|
||||
expect(inbox.sanitized_name).to eq('Test Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name becomes empty after sanitization' do
|
||||
context 'with email channel' do
|
||||
it 'falls back to email local part' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'support@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '\\<>@"', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Support')
|
||||
end
|
||||
|
||||
it 'handles email with complex local part' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'help-desk_team@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '!!!@@@', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Help Desk Team')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-email channel' do
|
||||
it 'returns empty string when name becomes blank' do
|
||||
web_widget_channel = FactoryBot.build(:channel_widget)
|
||||
inbox = FactoryBot.build(:inbox, name: '\\<>@"', channel: web_widget_channel)
|
||||
expect(inbox.sanitized_name).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name is blank initially' do
|
||||
context 'with email channel' do
|
||||
it 'uses email local part as fallback' do
|
||||
email_channel = FactoryBot.build(:channel_email, email: 'customer-care@example.com')
|
||||
inbox = FactoryBot.build(:inbox, name: '', channel: email_channel)
|
||||
expect(inbox.sanitized_name).to eq('Customer Care')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-email channel' do
|
||||
it 'returns empty string' do
|
||||
api_channel = FactoryBot.build(:channel_api)
|
||||
inbox = FactoryBot.build(:inbox, name: '', channel: api_channel)
|
||||
expect(inbox.sanitized_name).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name contains valid characters' do
|
||||
it 'preserves valid characters like hyphens, underscores, and dots' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test-Name_With.Valid-Characters')
|
||||
expect(inbox.sanitized_name).to eq('Test-Name_With.Valid-Characters')
|
||||
end
|
||||
|
||||
it 'preserves alphanumeric characters and spaces' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Customer Support 123')
|
||||
expect(inbox.sanitized_name).to eq('Customer Support 123')
|
||||
end
|
||||
|
||||
it 'preserves balanced safe characters but removes spam-trigger symbols' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Test!#$%&'*+/=?^_`{|}~-Name")
|
||||
expect(inbox.sanitized_name).to eq("Test'/_-Name")
|
||||
end
|
||||
|
||||
it 'keeps commonly used safe characters' do
|
||||
inbox = FactoryBot.build(:inbox, name: "Support/Help's Team.Desk_2024-Main")
|
||||
expect(inbox.sanitized_name).to eq("Support/Help's Team.Desk_2024-Main")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox name contains problematic characters for email headers' do
|
||||
it 'preserves Unicode symbols (trademark, etc.)' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test™Name®With©Special™Characters')
|
||||
expect(inbox.sanitized_name).to eq('Test™Name®With©Special™Characters')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with edge cases' do
|
||||
it 'handles nil name gracefully' do
|
||||
inbox = FactoryBot.build(:inbox)
|
||||
allow(inbox).to receive(:name).and_return(nil)
|
||||
expect { inbox.sanitized_name }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'handles very long names' do
|
||||
long_name = 'A' * 1000
|
||||
inbox = FactoryBot.build(:inbox, name: long_name)
|
||||
expect(inbox.sanitized_name).to eq(long_name)
|
||||
end
|
||||
|
||||
it 'handles unicode characters and preserves emojis' do
|
||||
inbox = FactoryBot.build(:inbox, name: 'Test Name with émojis 🎉')
|
||||
expect(inbox.sanitized_name).to eq('Test Name with émojis 🎉')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user