Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-rabbits-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Validates duplicated email and phone number when creating or editing omnichannel contacts.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ILivechatContact, Serialized } from '@rocket.chat/core-typings';
import { Field, FieldLabel, FieldRow, FieldError, TextInput, ButtonGroup, Button, IconButton, Divider } from '@rocket.chat/fuselage';
import { CustomFieldsForm } from '@rocket.chat/ui-client';
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useEndpoint, useSetModal } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { Fragment, useId } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
Expand Down Expand Up @@ -75,6 +75,7 @@ const EditContactInfo = ({ contactData, onClose, onCancel }: ContactNewEditProps

const editContact = useEditContact(['current-contacts']);
const createContact = useCreateContact(['current-contacts']);
const checkExistenceEndpoint = useEndpoint('GET', '/v1/omnichannel/contacts.checkExistence');

const handleOpenUpSellModal = () => setModal(<AdvancedContactModal onCancel={() => setModal(null)} />);

Expand Down Expand Up @@ -117,21 +118,47 @@ const EditContactInfo = ({ contactData, onClose, onCancel }: ContactNewEditProps
const { emails, phones } = watch();

const validateEmailFormat = async (emailValue: string) => {
if (!emails) {
return true;
}

const currentEmails = emails.map(({ address }) => address);
const isDuplicated = currentEmails.filter((email) => email === emailValue).length > 1;

if (!validateEmail(emailValue)) {
return t('error-invalid-email-address');
}

return !isDuplicated ? true : t('Email_already_exists');
if (currentEmails.filter((email) => email === emailValue).length > 1) {
return t('Email_already_exists');
}

const initialEmails = initialValue.emails.map(({ address }) => address);

if (!initialEmails.includes(emailValue) && (await checkExistenceEndpoint({ email: emailValue })).exists) {
return t('Email_already_exists');
}

return true;
};

const validatePhone = async (phoneValue: string) => {
if (!phones) {
return true;
}

const currentPhones = phones.map(({ phoneNumber }) => phoneNumber);
const isDuplicated = currentPhones.filter((phone) => phone === phoneValue).length > 1;

return !isDuplicated ? true : t('Phone_already_exists');
if (currentPhones.filter((phone) => phone === phoneValue).length > 1) {
return t('Phone_already_exists');
}

const initialPhones = initialValue.phones.map(({ phoneNumber }) => phoneNumber);

if (!initialPhones.includes(phoneValue) && (await checkExistenceEndpoint({ phone: phoneValue })).exists) {
return t('Phone_already_exists');
}

return true;
};

const validateName = (v: string): string | boolean => (!v.trim() ? t('Required_field', { field: t('Name') }) : true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ const createContact = (generateToken = false) => ({

const NEW_CONTACT = createContact();
const EDIT_CONTACT = createContact();
const EXISTING_CONTACT = createContact(true);
const EXISTING_CONTACT = {
id: undefined,
name: `${faker.person.firstName()} ${faker.person.lastName()}`,
emails: [faker.internet.email().toLowerCase()],
phones: [faker.phone.number('+############')],
token: undefined,
};
const NEW_CUSTOM_FIELD = {
searchable: true,
field: 'hiddenCustomField',
Expand All @@ -45,7 +51,9 @@ const ERROR = {
nameRequired: 'Name required',
invalidEmail: 'Invalid email address',
emailRequired: 'Email required',
emailAlreadyExists: 'Email already exists',
phoneRequired: 'Phone required',
phoneAlreadyExists: 'Phone already exists',
};

test.use({ storageState: Users.admin.state });
Expand All @@ -56,18 +64,18 @@ test.describe('Omnichannel Contact Center', () => {

test.beforeAll(async ({ api }) => {
// Add a contact
const { id: _, ...data } = EXISTING_CONTACT;
await api.post('/omnichannel/contacts', data);
await api.post('/omnichannel/contacts', EXISTING_CONTACT);

if (IS_EE) {
await api.post('/livechat/custom.field', NEW_CUSTOM_FIELD);
}
});

test.afterAll(async ({ api }) => {
// Remove added contacts
await api.delete(`/livechat/visitor/${EXISTING_CONTACT.token}`);
// Remove added contact
await api.delete(`/livechat/visitor/${NEW_CONTACT.token}`);
await api.delete(`/livechat/visitor/${EXISTING_CONTACT.token}`);

if (IS_EE) {
await api.post('method.call/livechat:removeCustomField', { message: NEW_CUSTOM_FIELD.field });
}
Expand All @@ -86,6 +94,14 @@ test.describe('Omnichannel Contact Center', () => {
NEW_CONTACT.token = res.contacts?.[0]?.token;
NEW_CONTACT.id = res.contacts?.[0]?._id;
});

await api
.get('/omnichannel/contacts.search', { searchText: EXISTING_CONTACT.phones[0] })
.then((res) => res.json())
.then((res) => {
EXISTING_CONTACT.token = res.contacts?.[0]?.token;
EXISTING_CONTACT.id = res.contacts?.[0]?._id;
});
});

test.beforeEach(async ({ page }) => {
Expand Down Expand Up @@ -122,15 +138,34 @@ test.describe('Omnichannel Contact Center', () => {
await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible();
});

await test.step('validate email is duplicated', async () => {
await poContacts.newContact.inputEmail.fill(EXISTING_CONTACT.emails[0]);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible();
});

await test.step('validate email is required', async () => {
await poContacts.newContact.inputEmail.clear();
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).toBeVisible();
});

await test.step('input email', async () => {
await poContacts.newContact.inputEmail.fill(NEW_CONTACT.email);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible();
await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible();
await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible();
});

await test.step('input phone', async () => {
await test.step('validate phone is duplicated', async () => {
await poContacts.newContact.btnAddPhone.click();
await poContacts.newContact.inputPhone.fill(EXISTING_CONTACT.phones[0]);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible();
});

await test.step('input phone', async () => {
await poContacts.newContact.inputPhone.fill(NEW_CONTACT.phone);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.phoneRequired)).not.toBeVisible();
Expand Down Expand Up @@ -173,9 +208,21 @@ test.describe('Omnichannel Contact Center', () => {
});

await test.step('validate email format', async () => {
await poContacts.contactInfo.inputEmail.fill('invalidemail');
await poContacts.newContact.inputEmail.fill('invalidemail');
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible();
});

await test.step('validate email is duplicated', async () => {
await poContacts.newContact.inputEmail.fill(EXISTING_CONTACT.emails[0]);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible();
});

await test.step('validate email is required', async () => {
await poContacts.newContact.inputEmail.clear();
await page.keyboard.press('Tab');
await expect(poContacts.contactInfo.getErrorMessage(ERROR.invalidEmail)).toBeVisible();
await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).toBeVisible();
});

await test.step('validate name is required', async () => {
Expand All @@ -188,6 +235,12 @@ test.describe('Omnichannel Contact Center', () => {
await poContacts.contactInfo.inputName.fill(EDIT_CONTACT.name);
});

await test.step('validate phone is duplicated', async () => {
await poContacts.newContact.inputPhone.fill(EXISTING_CONTACT.phones[0]);
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible();
});

await test.step('input phone ', async () => {
await poContacts.newContact.inputPhone.fill(EDIT_CONTACT.phone);
await page.keyboard.press('Tab');
Expand All @@ -199,6 +252,7 @@ test.describe('Omnichannel Contact Center', () => {
await page.keyboard.press('Tab');
await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible();
await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible();
await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible();
});

await test.step('save new contact ', async () => {
Expand Down
Loading