I’m hoping to use the @freshworks-jaya/freshchat-api
Freshchat API to pull the conversation messages in a serverless app once the conversation is complete. I got the code working just fine when I published it.
My struggle though has been writing a unit test and mocking the Freshchat API since it has to be instantiated as a new class instance in server.js. I’ve tried using Sinon to mock Freshchat, but it still ends up calling the real API and fails to connect since I’m mocking the API domain and key.
Does anyone have any suggestions on how to properly mock the Freshchat API in the test?
Here is my test (written in TS) using Sinon
server.ts
import {Iparams, ProductEventPayloadVanilla} from '../../src/server/interfaces/EventPayload';
import {
Agent,
Channel,
Group,
LabelCategory,
LabelSubcategory,
User,
Actor,
ConversationStatus,
ModelProperties,
ProductEventData,
ChangedStatus,
ResponseDueType,
Event
} from '@freshworks-jaya/marketplace-models';
import Freshchat from '@freshworks-jaya/freshchat-api';
const sinon = require('sinon');
describe('onConversationUpdateCallback', () => {
const conversationId = '3345234qabaca';
const api_domain = 'https://test.freshchat.com/v2';
const api_key = 'TEST API TOKEN';
const domain = 'http://google.com';
const iparams: Iparams = {
api_key: api_key,
api_domain: api_domain,
freshchat_domain: domain
};
let stub:any;
beforeEach(() => {
stub = sinon.stub(Freshchat.prototype, 'getConversationTranscript').returns("foo bar");
});
afterEach(() => {
stub.restore();
});
it('should call getConversationTranscript when conversation status is resolved', async function () {
const convoStatus = 'resolved';
const data: ProductEventData = getProductEventData(convoStatus, '2021-10-11 10:00:00', '2021-10-12 10:00:00', conversationId);
const payload: ProductEventPayloadVanilla = {
iparams: iparams,
data: data,
account_id: 'foo',
domain: 'foo',
event: <Event>{},
region: 'foo',
timestamp: 0,
version: 'foo',
};
await this.invoke('onConversationUpdate', payload);
sinon.assert.calledOnce(stub);
});
it('should NOT call getConversationTranscript when conversation status is new', async function () {
const convoStatus = 'new';
const data: ProductEventData = getProductEventData(convoStatus, '2021-10-11 10:00:00', '2021-10-12 10:00:00', conversationId);
const payload: ProductEventPayloadVanilla = {
iparams: iparams,
data: data,
account_id: 'foo',
domain: 'foo',
event: <Event>{},
region: 'foo',
timestamp: 0,
version: 'foo',
};
await this.invoke('onConversationUpdate', payload);
sinon.assert.notCalled(stub);
});
});
function getProductEventData(
status: string,
reopened_time: string,
created_time: string,
conversationId: string
) {
return <ProductEventData> {
actor: <Actor>{},
associations: {
agent: <Agent>{},
channel: <Channel>{},
group: <Group>{},
label_category: <LabelCategory>{},
label_subcategory: <LabelSubcategory>{},
user: <User>{},
},
changes: {
model_changes: {
assigned_agent_id: ['', ''],
assigned_group_id: ['', ''],
label_category_id: ['', ''],
label_subcategory_id: ['', ''],
status: [<ChangedStatus>{}, <ChangedStatus>{}]
}
},
conversation: {
app_id: 'foo',
assigned_agent_id: '',
assigned_group_id: '',
assigned_org_agent_id: '',
assigned_org_group_id: '',
assigned_time: '',
channel_id:'',
first_agent_assigned_time: '',
first_group_assigned_time: '',
group_assigned_time: '',
is_offline: false,
label_category_id: '',
label_subcategory_id: '',
resolved_time: '',
response_due_type: <ResponseDueType>{},
source: '',
statistics: {
agent_reassignment_time_bhrs: 0,
agent_reassignment_time_chrs: 0,
first_agent_assignment_time_bhrs: 0,
first_agent_assignment_time_chrs: 0,
first_group_assignment_time_bhrs: 0,
first_group_assignment_time_chrs: 0,
first_response_time_bhrs: 0,
first_response_time_chrs: 0,
group_reassignment_time_bhrs: 0,
group_reassignment_time_chrs: 0,
resolution_time_bhrs: 0,
resolution_time_chrs: 0,
wait_time_bhrs: 0,
wait_time_chrs: 0,
},
user_id: 'foo',
conversation_id: conversationId,
status: <ConversationStatus>status,
reopened_time: reopened_time,
created_time: created_time
},
message: <ModelProperties>{},
};
}
And here is my server.js file (written in TS)
server.ts
import {ProductEventPayloadVanilla} from './interfaces/EventPayload';
import Freshchat from '@freshworks-jaya/freshchat-api';
interface SalesforceMessage {
user_email?: string;
user_first_name?: string;
user_last_name?: string;
agent_email?: string;
agent_first_name?: string;
agent_last_name?: string;
conversation_timestamp?: string;
conversation_text?: string;
}
const appAlias = '<APP_ALIAS>';
/**
* Get conversation message thread using Freshchat API
*
* @param payload - event payload
*
* @return message thread as text
*/
const getConversationThread = async (
payload: ProductEventPayloadVanilla
): Promise<string> => {
try {
const freshchat = new Freshchat(
payload.iparams.api_domain,
payload.iparams.api_key
);
const appUrl = payload.iparams.freshchat_domain;
const conversationId = payload.data.conversation.conversation_id;
return freshchat.getConversationTranscript(
appUrl,
appAlias,
conversationId,
{
output: 'text',
isIncludeFreshchatLink: false,
isFetchUntilLastResolve: true,
timezoneOffset: 0,
messagesLimit: 200,
},
{
isExcludeNormal: false,
isExcludePrivate: false,
isExcludeSystem: true,
}
);
} catch (error) {
console.error(error);
return Promise.reject(
'Error in making get request to Freshchat to fetch conversation messages'
);
}
};
exports = {
/**
* onConversationUpdate callback method
* (configured in manifest.json)
*
* @param payload - event payload
*/
onConversationUpdateCallback: async function(
payload: ProductEventPayloadVanilla
) {
const conversationStatus = payload.data.conversation.status;
if (conversationStatus !== 'resolved') {
return;
}
try {
const thread: string = await getConversationThread(payload);
const resolvedTimestamp = payload.data.conversation.reopened_time;
const createdTimestamp = payload.data.conversation.created_time;
const conversationTimestamp = resolvedTimestamp
? resolvedTimestamp
: createdTimestamp;
const message: SalesforceMessage = {
user_email: payload.data.associations.user.email,
user_first_name: payload.data.associations.user.first_name,
user_last_name: payload.data.associations.user.last_name,
agent_email: payload.data.associations.agent.email,
agent_first_name: payload.data.associations.agent.first_name,
agent_last_name: payload.data.associations.agent.last_name,
conversation_timestamp: conversationTimestamp,
conversation_text: thread,
};
console.log(JSON.stringify(message));
} catch (error) {
console.log('Conversation not found');
}
},
};