chore: Enhance contact merge action for identified users (#4886)

- Discard conflicting keys 
- Do not merge if there is already an identified contact

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose 2022-06-23 15:48:56 +05:30 committed by GitHub
parent d5ddc9d76c
commit f71980bd95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 311 additions and 114 deletions

View File

@ -1,7 +1,16 @@
# retain_original_contact_name: false / true
# In case of setUser we want to update the name of the identified contact,
# which is the default behaviour
#
# But, In case of contact merge during prechat form contact update.
# We don't want to update the name of the identified original contact.
class ContactIdentifyAction class ContactIdentifyAction
pattr_initialize [:contact!, :params!] pattr_initialize [:contact!, :params!, { retain_original_contact_name: false }]
def perform def perform
@attributes_to_update = [:identifier, :name, :email, :phone_number]
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
merge_if_existing_identified_contact merge_if_existing_identified_contact
merge_if_existing_email_contact merge_if_existing_email_contact
@ -18,49 +27,88 @@ class ContactIdentifyAction
end end
def merge_if_existing_identified_contact def merge_if_existing_identified_contact
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact) return unless merge_contacts?(existing_identified_contact, :identifier)
process_contact_merge(existing_identified_contact)
end end
def merge_if_existing_email_contact def merge_if_existing_email_contact
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact) return unless merge_contacts?(existing_email_contact, :email)
process_contact_merge(existing_email_contact)
end end
def merge_if_existing_phone_number_contact def merge_if_existing_phone_number_contact
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact) return unless merge_contacts?(existing_phone_number_contact, :phone_number)
return unless mergable_phone_contact?
process_contact_merge(existing_phone_number_contact)
end
def process_contact_merge(mergee_contact)
@contact = merge_contact(mergee_contact, @contact)
@attributes_to_update.delete(:name) if retain_original_contact_name
end end
def existing_identified_contact def existing_identified_contact
return if params[:identifier].blank? return if params[:identifier].blank?
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier]) @existing_identified_contact ||= account.contacts.find_by(identifier: params[:identifier])
end end
def existing_email_contact def existing_email_contact
return if params[:email].blank? return if params[:email].blank?
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email]) @existing_email_contact ||= account.contacts.find_by(email: params[:email])
end end
def existing_phone_number_contact def existing_phone_number_contact
return if params[:phone_number].blank? return if params[:phone_number].blank?
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number]) @existing_phone_number_contact ||= account.contacts.find_by(phone_number: params[:phone_number])
end end
def merge_contacts?(existing_contact, _contact) def merge_contacts?(existing_contact, key)
existing_contact && existing_contact.id != @contact.id return if existing_contact.blank?
return true if params[:identifier].blank?
# we want to prevent merging contacts with different identifiers
if existing_contact.identifier.present? && existing_contact.identifier != params[:identifier]
# we will remove attribute from update list
@attributes_to_update.delete(key)
return false
end
true
end
# case: contact 1: email: 1@test.com, phone: 123456789
# params: email: 2@test.com, phone: 123456789
# we don't want to overwrite 1@test.com since email parameter takes higer priority
def mergable_phone_contact?
return true if params[:email].blank?
if existing_phone_number_contact.email.present? && existing_phone_number_contact.email != params[:email]
@attributes_to_update.delete(:phone_number)
return false
end
true
end end
def update_contact def update_contact
params_to_update = params.slice(*@attributes_to_update).reject do |_k, v|
v.blank?
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })
# blank identifier or email will throw unique index error # blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v| @contact.update!(params_to_update)
v.blank?
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
return base_contact if base_contact.id == merge_contact.id
ContactMergeAction.new( ContactMergeAction.new(
account: account, account: account,
base_contact: base_contact, base_contact: base_contact,
@ -69,14 +117,14 @@ class ContactIdentifyAction
end end
def custom_attributes def custom_attributes
params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes return @contact.custom_attributes if params[:custom_attributes].blank?
(@contact.custom_attributes || {}).deep_merge(params[:custom_attributes].stringify_keys)
end end
def additional_attributes def additional_attributes
if params[:additional_attributes] return @contact.additional_attributes if params[:additional_attributes].blank?
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
else (@contact.additional_attributes || {}).deep_merge(params[:additional_attributes].stringify_keys)
@contact.additional_attributes
end
end end
end end

View File

@ -44,38 +44,6 @@ class Api::V1::Widget::BaseController < ApplicationController
} }
end end
def update_contact(email)
contact_with_email = @current_account.contacts.find_by(email: email)
if contact_with_email
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_email,
mergee_contact: @contact
).perform
else
@contact.update!(email: email)
update_contact_name
end
end
def update_contact_phone_number(phone_number)
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
if contact_with_phone_number
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_phone_number,
mergee_contact: @contact
).perform
else
@contact.update!(phone_number: phone_number)
update_contact_name
end
end
def update_contact_name
@contact.update!(name: contact_name) if contact_name.present?
end
def contact_email def contact_email
permitted_params.dig(:contact, :email)&.downcase permitted_params.dig(:contact, :email)&.downcase
end end

View File

@ -1,14 +1,27 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :process_hmac, only: [:update] include WidgetHelper
before_action :validate_hmac, only: [:set_user]
def show; end def show; end
def update def update
contact_identify_action = ContactIdentifyAction.new( identify_contact(@contact)
contact: @contact, end
params: permitted_params.to_h.deep_symbolize_keys
) def set_user
@contact = contact_identify_action.perform contact = nil
if a_different_contact?
@contact_inbox, @widget_auth_token = build_contact_inbox_with_token(@web_widget)
contact = @contact_inbox.contact
else
contact = @contact
end
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
identify_contact(contact)
end end
# TODO : clean up this with proper routes delete contacts/custom_attributes # TODO : clean up this with proper routes delete contacts/custom_attributes
@ -20,12 +33,22 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
private private
def process_hmac def identify_contact(contact)
contact_identify_action = ContactIdentifyAction.new(
contact: contact,
params: permitted_params.to_h.deep_symbolize_keys
)
@contact = contact_identify_action.perform
end
def a_different_contact?
@contact.identifier.present? && @contact.identifier != permitted_params[:identifier]
end
def validate_hmac
return unless should_verify_hmac? return unless should_verify_hmac?
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac? render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
@contact_inbox.update(hmac_verified: true)
end end
def should_verify_hmac? def should_verify_hmac?

View File

@ -14,8 +14,11 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end end
def process_update_contact def process_update_contact
update_contact(contact_email) if @contact.email.blank? && contact_email.present? @contact = ContactIdentifyAction.new(
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present? contact: @contact,
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
retain_original_contact_name: true
).perform
end end
def update_last_seen def update_last_seen

View File

@ -15,7 +15,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def update def update
if @message.content_type == 'input_email' if @message.content_type == 'input_email'
@message.update!(submitted_email: contact_email) @message.update!(submitted_email: contact_email)
update_contact(contact_email) ContactIdentifyAction.new(
contact: @contact,
params: { email: contact_email }
).perform
else else
@message.update!(message_update_params[:message]) @message.update!(message_update_params[:message])
end end

View File

@ -1,5 +1,7 @@
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged # TODO : Delete this and associated spec once 'api/widget/config' end point is merged
class WidgetsController < ActionController::Base class WidgetsController < ActionController::Base
include WidgetHelper
before_action :set_global_config before_action :set_global_config
before_action :set_web_widget before_action :set_web_widget
before_action :set_token before_action :set_token
@ -40,11 +42,8 @@ class WidgetsController < ActionController::Base
def build_contact def build_contact
return if @contact.present? return if @contact.present?
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes) @contact_inbox, @token = build_contact_inbox_with_token(@web_widget, additional_attributes)
@contact = @contact_inbox.contact @contact = @contact_inbox.contact
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
@token = ::Widget::TokenService.new(payload: payload).generate_token
end end
def additional_attributes def additional_attributes

View File

@ -0,0 +1,9 @@
module WidgetHelper
def build_contact_inbox_with_token(web_widget, additional_attributes = {})
contact_inbox = web_widget.create_contact_inbox(additional_attributes)
payload = { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id }
token = ::Widget::TokenService.new(payload: payload).generate_token
[contact_inbox, token]
end
end

View File

@ -33,6 +33,12 @@ import {
import { isFlatWidgetStyle } from './settingsHelper'; import { isFlatWidgetStyle } from './settingsHelper';
import { popoutChatWindow } from '../widget/helpers/popoutHelper'; import { popoutChatWindow } from '../widget/helpers/popoutHelper';
const updateAuthCookie = cookieContent =>
Cookies.set('cw_conversation', cookieContent, {
expires: 365,
sameSite: 'Lax',
});
export const IFrameHelper = { export const IFrameHelper = {
getUrl({ baseUrl, websiteToken }) { getUrl({ baseUrl, websiteToken }) {
return `${baseUrl}/widget?website_token=${websiteToken}`; return `${baseUrl}/widget?website_token=${websiteToken}`;
@ -134,10 +140,7 @@ export const IFrameHelper = {
events: { events: {
loaded: message => { loaded: message => {
Cookies.set('cw_conversation', message.config.authToken, { updateAuthCookie(message.config.authToken);
expires: 365,
sameSite: 'Lax',
});
window.$chatwoot.hasLoaded = true; window.$chatwoot.hasLoaded = true;
IFrameHelper.sendMessage('config-set', { IFrameHelper.sendMessage('config-set', {
locale: window.$chatwoot.locale, locale: window.$chatwoot.locale,
@ -178,6 +181,10 @@ export const IFrameHelper = {
setBubbleText(window.$chatwoot.launcherTitle || message.label); setBubbleText(window.$chatwoot.launcherTitle || message.label);
}, },
setAuthCookie({ data: { widgetAuthToken } }) {
updateAuthCookie(widgetAuthToken);
},
toggleBubble: state => { toggleBubble: state => {
let bubbleState = {}; let bubbleState = {};
if (state === 'open') { if (state === 'open') {

View File

@ -83,12 +83,11 @@ export default {
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel; const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
this.setLocale(locale); this.setLocale(locale);
this.setWidgetColor(widgetColor); this.setWidgetColor(widgetColor);
setHeader(window.authToken);
if (this.isIFrame) { if (this.isIFrame) {
this.registerListeners(); this.registerListeners();
this.sendLoadedEvent(); this.sendLoadedEvent();
setHeader('X-Auth-Token', window.authToken);
} else { } else {
setHeader('X-Auth-Token', window.authToken);
this.fetchOldConversations(); this.fetchOldConversations();
this.fetchAvailableAgents(websiteToken); this.fetchAvailableAgents(websiteToken);
this.setLocale(getLocale(window.location.search)); this.setLocale(getLocale(window.location.search));
@ -253,7 +252,7 @@ export default {
} else if (message.event === 'remove-label') { } else if (message.event === 'remove-label') {
this.$store.dispatch('conversationLabels/destroy', message.label); this.$store.dispatch('conversationLabels/destroy', message.label);
} else if (message.event === 'set-user') { } else if (message.event === 'set-user') {
this.$store.dispatch('contacts/update', message); this.$store.dispatch('contacts/setUser', message);
} else if (message.event === 'set-custom-attributes') { } else if (message.event === 'set-custom-attributes') {
this.$store.dispatch( this.$store.dispatch(
'contacts/setCustomAttributes', 'contacts/setCustomAttributes',

View File

@ -6,8 +6,11 @@ export default {
get() { get() {
return API.get(buildUrl('widget/contact')); return API.get(buildUrl('widget/contact'));
}, },
update(identifier, userObject) { update(userObject) {
return API.patch(buildUrl('widget/contact'), { return API.patch(buildUrl('widget/contact'), userObject);
},
setUser(identifier, userObject) {
return API.patch(buildUrl('widget/contact/set_user'), {
identifier, identifier,
...userObject, ...userObject,
}); });

View File

@ -6,7 +6,7 @@ export const API = axios.create({
withCredentials: false, withCredentials: false,
}); });
export const setHeader = (key, value) => { export const setHeader = (value, key = 'X-Auth-Token') => {
API.defaults.headers.common[key] = value; API.defaults.headers.common[key] = value;
}; };

View File

@ -10,14 +10,16 @@ export const arrayToHashById = array =>
return newMap; return newMap;
}, {}); }, {});
export const IFrameHelper = { export const sendMessage = msg => {
isIFrame: () => window.self !== window.top,
sendMessage: msg => {
window.parent.postMessage( window.parent.postMessage(
`chatwoot-widget:${JSON.stringify({ ...msg })}`, `chatwoot-widget:${JSON.stringify({ ...msg })}`,
'*' '*'
); );
}, };
export const IFrameHelper = {
isIFrame: () => window.self !== window.top,
sendMessage,
isAValidEvent: e => { isAValidEvent: e => {
const isDataAString = typeof e.data === 'string'; const isDataAString = typeof e.data === 'string';
const isAValidWootEvent = const isAValidWootEvent =

View File

@ -1,11 +1,23 @@
import { IFrameHelper } from 'widget/helpers/utils'; import { sendMessage } from 'widget/helpers/utils';
import ContactsAPI from '../../api/contacts'; import ContactsAPI from '../../api/contacts';
import { SET_USER_ERROR } from '../../constants/errorTypes'; import { SET_USER_ERROR } from '../../constants/errorTypes';
import { setHeader } from '../../helpers/axios';
const state = { const state = {
currentUser: {}, currentUser: {},
}; };
const SET_CURRENT_USER = 'SET_CURRENT_USER'; const SET_CURRENT_USER = 'SET_CURRENT_USER';
const parseErrorData = error =>
error && error.response && error.response.data ? error.response.data : error;
export const updateWidgetAuthToken = widgetAuthToken => {
if (widgetAuthToken) {
setHeader(widgetAuthToken);
sendMessage({
event: 'setAuthCookie',
data: { widgetAuthToken },
});
}
};
export const getters = { export const getters = {
getCurrentUser(_state) { getCurrentUser(_state) {
@ -22,13 +34,21 @@ export const actions = {
// Ignore error // Ignore error
} }
}, },
update: async ({ dispatch }, { identifier, user: userObject }) => { update: async ({ dispatch }, { user }) => {
try {
await ContactsAPI.update(user);
dispatch('get');
} catch (error) {
// Ignore error
}
},
setUser: async ({ dispatch }, { identifier, user: userObject }) => {
try { try {
const { const {
email, email,
name, name,
avatar_url, avatar_url,
identifier_hash, identifier_hash: identifierHash,
phone_number, phone_number,
company_name, company_name,
city, city,
@ -41,7 +61,7 @@ export const actions = {
email, email,
name, name,
avatar_url, avatar_url,
identifier_hash, identifier_hash: identifierHash,
phone_number, phone_number,
additional_attributes: { additional_attributes: {
company_name, company_name,
@ -52,24 +72,19 @@ export const actions = {
}, },
custom_attributes, custom_attributes,
}; };
await ContactsAPI.update(identifier, user); const {
data: { widget_auth_token: widgetAuthToken },
} = await ContactsAPI.setUser(identifier, user);
updateWidgetAuthToken(widgetAuthToken);
dispatch('get'); dispatch('get');
if (identifier_hash) { if (identifierHash || widgetAuthToken) {
dispatch('conversation/clearConversations', {}, { root: true }); dispatch('conversation/clearConversations', {}, { root: true });
dispatch('conversation/fetchOldConversations', {}, { root: true }); dispatch('conversation/fetchOldConversations', {}, { root: true });
dispatch('conversationAttributes/getAttributes', {}, { root: true }); dispatch('conversationAttributes/getAttributes', {}, { root: true });
} }
} catch (error) { } catch (error) {
const data = const data = parseErrorData(error);
error && error.response && error.response.data sendMessage({ event: 'error', errorType: SET_USER_ERROR, data });
? error.response.data
: error;
IFrameHelper.sendMessage({
event: 'error',
errorType: SET_USER_ERROR,
data,
});
} }
}, },
setCustomAttributes: async (_, customAttributes = {}) => { setCustomAttributes: async (_, customAttributes = {}) => {

View File

@ -1,21 +1,26 @@
import { API } from 'widget/helpers/axios'; import { API } from 'widget/helpers/axios';
import { sendMessage } from 'widget/helpers/utils';
import { actions } from '../../contacts'; import { actions } from '../../contacts';
const commit = jest.fn(); const commit = jest.fn();
const dispatch = jest.fn(); const dispatch = jest.fn();
jest.mock('widget/helpers/axios'); jest.mock('widget/helpers/axios');
jest.mock('widget/helpers/utils', () => ({
sendMessage: jest.fn(),
}));
describe('#actions', () => { describe('#actions', () => {
describe('#update', () => { describe('#setUser', () => {
it('sends correct actions', async () => { it('sends correct actions if contact object is refreshed ', async () => {
const user = { const user = {
email: 'thoma@sphadikam.com', email: 'thoma@sphadikam.com',
name: 'Adu Thoma', name: 'Adu Thoma',
avatar_url: '', avatar_url: '',
identifier_hash: 'random_hex_identifier_hash',
}; };
API.patch.mockResolvedValue({ data: { pubsub_token: 'token' } }); API.patch.mockResolvedValue({ data: { widget_auth_token: 'token' } });
await actions.update({ commit, dispatch }, { identifier: 1, user }); await actions.setUser({ commit, dispatch }, { identifier: 1, user });
expect(sendMessage.mock.calls).toEqual([
[{ data: { widgetAuthToken: 'token' }, event: 'setAuthCookie' }],
]);
expect(commit.mock.calls).toEqual([]); expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([ expect(dispatch.mock.calls).toEqual([
['get'], ['get'],
@ -24,5 +29,52 @@ describe('#actions', () => {
['conversationAttributes/getAttributes', {}, { root: true }], ['conversationAttributes/getAttributes', {}, { root: true }],
]); ]);
}); });
it('sends correct actions if identifierHash is passed ', async () => {
const user = {
email: 'thoma@sphadikam.com',
name: 'Adu Thoma',
avatar_url: '',
identifier_hash: '12345',
};
API.patch.mockResolvedValue({ data: { id: 1 } });
await actions.setUser({ commit, dispatch }, { identifier: 1, user });
expect(sendMessage.mock.calls).toEqual([]);
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([
['get'],
['conversation/clearConversations', {}, { root: true }],
['conversation/fetchOldConversations', {}, { root: true }],
['conversationAttributes/getAttributes', {}, { root: true }],
]);
});
it('does not call sendMessage if contact object is not refreshed ', async () => {
const user = {
email: 'thoma@sphadikam.com',
name: 'Adu Thoma',
avatar_url: '',
};
API.patch.mockResolvedValue({ data: { id: 1 } });
await actions.setUser({ commit, dispatch }, { identifier: 1, user });
expect(sendMessage.mock.calls).toEqual([]);
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([['get']]);
});
});
describe('#update', () => {
it('sends correct actions', async () => {
const user = {
email: 'thoma@sphadikam.com',
name: 'Adu Thoma',
avatar_url: '',
identifier_hash: 'random_hex_identifier_hash',
};
API.patch.mockResolvedValue({ data: { id: 1 } });
await actions.update({ commit, dispatch }, { identifier: 1, user });
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([['get']]);
});
}); });
}); });

View File

@ -0,0 +1,5 @@
json.id @contact.id
json.name @contact.name
json.email @contact.email
json.phone_number @contact.phone_number
json.widget_auth_token @widget_auth_token if @widget_auth_token.present?

View File

@ -199,6 +199,7 @@ Rails.application.routes.draw do
resource :contact, only: [:show, :update] do resource :contact, only: [:show, :update] do
collection do collection do
post :destroy_custom_attributes post :destroy_custom_attributes
patch :set_user
end end
end end
resources :inbox_members, only: [:index] resources :inbox_members, only: [:index]

View File

@ -48,13 +48,22 @@ describe ::ContactIdentifyAction do
context 'when contact with same email exists' do context 'when contact with same email exists' do
it 'merges the current contact to email contact' do it 'merges the current contact to email contact' do
existing_email_contact = create(:contact, account: account, email: 'test@test.com') existing_email_contact = create(:contact, account: account, email: 'test@test.com', name: 'old name')
params = { email: 'test@test.com' } params = { name: 'new name', email: 'test@test.com' }
result = described_class.new(contact: contact, params: params).perform result = described_class.new(contact: contact, params: params).perform
expect(result.id).to eq existing_email_contact.id expect(result.id).to eq existing_email_contact.id
expect(result.name).to eq existing_email_contact.name expect(result.name).to eq 'new name'
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
it 'will not merge the current contact to email contact if identifier of email contact is different' do
existing_email_contact = create(:contact, account: account, identifier: '1', email: 'test@test.com')
params = { identifier: '2', email: 'test@test.com' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).not_to eq existing_email_contact.id
expect(result.identifier).to eq params[:identifier]
expect(result.email).to eq nil
end
end end
context 'when contact with same phone_number exists' do context 'when contact with same phone_number exists' do
@ -66,6 +75,24 @@ describe ::ContactIdentifyAction do
expect(result.name).to eq existing_phone_number_contact.name expect(result.name).to eq existing_phone_number_contact.name
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
it 'will not merge the current contact to phone contact if identifier of phone contact is different' do
existing_phone_number_contact = create(:contact, account: account, identifier: '1', phone_number: '+919999888877')
params = { identifier: '2', phone_number: '+919999888877' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).not_to eq existing_phone_number_contact.id
expect(result.identifier).to eq params[:identifier]
expect(result.email).to eq nil
end
it 'will not overide the phone contacts email when params contains different email' do
existing_phone_number_contact = create(:contact, account: account, email: '1@test.com', phone_number: '+919999888877')
params = { email: '2@test.com', phone_number: '+919999888877' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).not_to eq existing_phone_number_contact.id
expect(result.email).to eq params[:email]
expect(result.phone_number).to eq nil
end
end end
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
@ -77,5 +104,16 @@ describe ::ContactIdentifyAction do
expect(result.name).to eq 'new name' expect(result.name).to eq 'new name'
end end
end end
context 'when retain_original_contact_name is set to true' do
it 'will not update the name of the existing contact' do
existing_email_contact = create(:contact, account: account, name: 'old name', email: 'test@test.com')
params = { email: 'test@test.com', name: 'new name' }
result = described_class.new(contact: contact, params: params, retain_original_contact_name: true).perform
expect(result.id).to eq existing_email_contact.id
expect(result.name).to eq 'old name'
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end end
end end

View File

@ -82,20 +82,42 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
end end
end end
end
context 'with mandatory hmac' do describe 'PATCH /api/v1/widget/contact/set_user' do
let(:identify_action) { double } let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
let(:web_widget) { create(:channel_widget, account: account, hmac_mandatory: true) } let(:web_widget) { create(:channel_widget, account: account, hmac_mandatory: true) }
let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, params[:identifier].to_s) } let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, params[:identifier].to_s) }
let(:incorrect_identifier_hash) { 'test' } let(:incorrect_identifier_hash) { 'test' }
context 'when the current contact identifier is different from param identifier' do
before do
contact.update(identifier: 'random')
end
it 'return a new contact for the provided identifier' do
patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: correct_identifier_hash),
headers: { 'X-Auth-Token' => token },
as: :json
body = JSON.parse(response.body)
expect(body['id']).not_to eq(contact.id)
expect(body['widget_auth_token']).not_to eq(nil)
expect(Contact.find(body['id']).contact_inboxes.first.hmac_verified?).to eq(true)
end
end
context 'with mandatory hmac' do
let(:identify_action) { double }
before do before do
allow(ContactIdentifyAction).to receive(:new).and_return(identify_action) allow(ContactIdentifyAction).to receive(:new).and_return(identify_action)
allow(identify_action).to receive(:perform).and_return(contact) allow(identify_action).to receive(:perform).and_return(contact)
end end
it 'returns success when correct identifier hash is provided' do it 'returns success when correct identifier hash is provided' do
patch '/api/v1/widget/contact', patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: correct_identifier_hash), params: params.merge(identifier_hash: correct_identifier_hash),
headers: { 'X-Auth-Token' => token }, headers: { 'X-Auth-Token' => token },
as: :json as: :json
@ -104,7 +126,7 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
end end
it 'returns error when incorrect identifier hash is provided' do it 'returns error when incorrect identifier hash is provided' do
patch '/api/v1/widget/contact', patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: incorrect_identifier_hash), params: params.merge(identifier_hash: incorrect_identifier_hash),
headers: { 'X-Auth-Token' => token }, headers: { 'X-Auth-Token' => token },
as: :json as: :json
@ -113,7 +135,7 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
end end
it 'returns error when identifier hash is blank' do it 'returns error when identifier hash is blank' do
patch '/api/v1/widget/contact', patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: ''), params: params.merge(identifier_hash: ''),
headers: { 'X-Auth-Token' => token }, headers: { 'X-Auth-Token' => token },
as: :json as: :json
@ -122,7 +144,7 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
end end
it 'returns error when identifier hash is not provided' do it 'returns error when identifier hash is not provided' do
patch '/api/v1/widget/contact', patch '/api/v1/widget/contact/set_user',
params: params, params: params,
headers: { 'X-Auth-Token' => token }, headers: { 'X-Auth-Token' => token },
as: :json as: :json