UNPKG

bot-handoff

Version:

Bot hand off module for the Microsoft Bot Framework. It allows you to transfer a customer from talking to a bot to talking to a human.

386 lines (319 loc) 18.4 kB
import * as Promise from 'bluebird'; import { BotTester } from 'bot-tester'; import { ConsoleConnector, IAddress, IMessage, Message, Session, UniversalBot } from 'botbuilder'; import * as chai from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { setTimeout } from 'timers'; import { QueueEventMessage } from '../src/eventMessages/QueueEventMessage'; import { EventFailureHandler } from '../src/options/EventFailureHandlers'; import { EventSuccessHandler } from '../src/options/EventSuccessHandlers'; import { IEventHandler, IEventHandlers } from '../src/options/IEventHandlers'; import { InMemoryProvider } from '../src/provider/prebuilt/InMemoryProvider'; import { applyHandoffMiddleware } from './../src/applyHandoffMiddleware'; import { ConnectEventMessage } from './../src/eventMessages/ConnectEventMessage'; import { DequeueEventMessage } from './../src/eventMessages/DequeueEventMessage'; import { DisconnectEventMessage } from './../src/eventMessages/DisconnectEventMessage'; import { ErrorEventMessage } from './../src/eventMessages/ErrorEventMessage'; import { HandoffEventMessage } from './../src/eventMessages/HandoffEventMessage'; import { UnwatchEventMessage } from './../src/eventMessages/UnwatchEventMessage'; import { WatchEventMessage } from './../src/eventMessages/WatchEventMessage'; import { ConversationState, IConversation } from './../src/IConversation'; import { addCustomerAddressToMessage, IHandoffMessage } from './../src/IHandoffMessage'; import { AgentAlreadyInConversationError } from './../src/provider/errors/AgentAlreadyInConversationError'; import { ConversationStateUnchangedException } from './../src/provider/errors/ConversationStateUnchangedException'; import { IProvider } from './../src/provider/IProvider'; chai.use(sinonChai); const expect = chai.expect; const connector = new ConsoleConnector(); const CUSTOMER_ADDRESS: IAddress = { channelId: 'console', user: { id: 'userId1', name: 'user1' }, bot: { id: 'bot', name: 'Bot' }, conversation: { id: 'user1Conversation' } }; const AGENT_ADDRESS: IAddress = { channelId: 'console', user: { id: 'agentId', name: 'agent' }, bot: { id: 'bot', name: 'Bot' }, conversation: { id: 'agent_convo' } }; const AGENT_ADDRESS_2: IAddress = { channelId: 'console', user: { id: 'agentId2', name: 'agent2' }, bot: { id: 'bot', name: 'Bot' }, conversation: { id: 'agent_convo2' } }; const isAgent = (session: Session): Promise<boolean> => { return Promise.resolve(!!(session.message as IHandoffMessage).agentAddress); }; //tslint:disable function createIProviderSpy(provider: IProvider): IProvider { Object.getOwnPropertyNames(Object.getPrototypeOf(provider)).forEach((method: string) => { provider[method] = sinon.spy(provider, method as any); }); return provider; } //tslint:enable function createEventHandlerSpy(): IEventHandler { return { success: sinon.spy() as EventSuccessHandler, failure: sinon.spy() as EventFailureHandler }; } function createEventHandlerSpies(): IEventHandlers { return { connect: createEventHandlerSpy(), disconnect: createEventHandlerSpy(), queue: createEventHandlerSpy(), dequeue: createEventHandlerSpy(), watch: createEventHandlerSpy(), unwatch: createEventHandlerSpy() }; } function expectConvoIsInWaitAndWatchState(convo: IConversation): void { expect(convo.agentAddress).to.deep.equal(AGENT_ADDRESS); expect(convo.customerAddress).to.deep.equal(CUSTOMER_ADDRESS); expect(convo.conversationState).to.be.equal(ConversationState.WatchAndWait); } function expectCallCount(count: number, ...spies: {}[]): void { spies.forEach((spy: {}) => expect(spy).to.have.been.callCount(count)); } function expectZeroCallsToSpies(...spies: {}[]): void { expectCallCount(0, ...spies); } describe('event message', () => { let bot: UniversalBot; let provider: IProvider; let eventMessage: HandoffEventMessage; let convo: IConversation; // actually a spy, but allows us to group the related spies and pass them off as the existing functions // let successHandlerSpies: EventSuccessHandlers; let eventHandlerSpies: IEventHandlers; // actually a spy, but this allows us to only focus on the relevant methods let providerSpy: IProvider; const customerIntroMessage = new Message() .text('hello') .address(CUSTOMER_ADDRESS) .toMessage(); beforeEach(() => { provider = new InMemoryProvider(); providerSpy = createIProviderSpy(provider); eventHandlerSpies = createEventHandlerSpies(); bot = new UniversalBot(connector); bot.dialog('/', (session: Session) => { session.send('intro!'); }); const handoffOptions = { eventHandlers: eventHandlerSpies }; applyHandoffMiddleware(bot, isAgent, provider, handoffOptions); return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(customerIntroMessage) .runTest(); }); afterEach(() => { ensureProviderDidNotTranscribeMessage(eventMessage); }); function ensureProviderDidNotTranscribeMessage(msg: HandoffEventMessage): void { expect(provider.addAgentMessageToTranscript).not.to.have.been.calledWith(msg); expect(provider.addBotMessageToTranscript).not.to.have.been.calledWith(msg); expect(provider.addCustomerMessageToTranscript).not.to.have.been.calledWith(msg); } function sendMessageToBotAndGetConversationData( msg: HandoffEventMessage, expectedResponse?: string | IMessage): Promise<IConversation> { return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(msg, expectedResponse) .runTest() .then(() => provider.getConversationFromCustomerAddress(CUSTOMER_ADDRESS)); } describe('connect/disconnect', () => { beforeEach(() => { eventMessage = new ConnectEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((conversation: IConversation) => convo = conversation); }); it('connect sets converation state to Agent and calls the connect success event handler', () => { expect(convo.conversationState).to.be.equal(ConversationState.Agent); expect(providerSpy.connectCustomerToAgent).to.have.been.calledWith(CUSTOMER_ADDRESS, AGENT_ADDRESS); expect(eventHandlerSpies.connect.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.connect.success).to.have.been.calledOnce; expect(eventHandlerSpies.connect.failure).not.to.have.been.calledOnce; }); it('disconnect sets converation state to Bot and calls the disconnect success event handler', () => { eventMessage = new DisconnectEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((conversation: IConversation) => { expect(conversation.conversationState).to.be.equal(ConversationState.Bot); expect(providerSpy.disconnectCustomerFromAgent).to.have.been.calledWith(CUSTOMER_ADDRESS, AGENT_ADDRESS); expect(eventHandlerSpies.disconnect.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.disconnect.success).to.have.been.calledOnce; expect(eventHandlerSpies.disconnect.failure).not.to.have.been.called; }); }); //tslint:disable it('sending connect event to an already connected conversation responds with an error event to the requesting agent and the success event handler is not called', () => { //tslint:enable const errorEventMessage = new ErrorEventMessage(eventMessage, new AgentAlreadyInConversationError(AGENT_ADDRESS.conversation.id)); const errorMessage = new ErrorEventMessage(eventMessage, 'some error goes here'); return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(eventMessage, errorEventMessage) .runTest() .then(() => { expect(eventHandlerSpies.connect.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.connect.success).to.have.been.calledOnce; expect(eventHandlerSpies.connect.failure).to.have.been.calledOnce; }); }); //tslint:disable it('throws a CustomerAlreadyConnectedException to an agent that attempts to connect to a user that is already connect to another agent', () => { //tslint:enable const connectionEvent1 = new ConnectEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); const connectionEvent2 = new ConnectEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS_2); const errorEventMessage = new ErrorEventMessage(connectionEvent2, { name: 'CustomerAlreadyConnectedException' }); return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(connectionEvent2, errorEventMessage) .runTest() .then(() => { expect(eventHandlerSpies.connect.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.connect.success).to.have.been.calledOnce; expect(eventHandlerSpies.connect.failure).to.have.been.calledOnce; }); }); }); describe('watch/unwatch', () => { beforeEach(() => { eventMessage = new WatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((conversation: IConversation) => convo = conversation); }); it('watch sets the conversation state to watch when in bot state', () => { expect(convo.conversationState).to.be.equal(ConversationState.Watch); expect(convo.customerAddress).to.be.equal(CUSTOMER_ADDRESS); expect(convo.agentAddress).to.deep.equal(AGENT_ADDRESS); expect(eventHandlerSpies.watch.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.watch.success).to.have.been.calledOnce; expect(eventHandlerSpies.watch.failure).not.to.have.been.called; }); it('unwatch sets the conversation state to bot when in a watch state', () => { eventMessage = new UnwatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((conversationt: IConversation) => { expect(conversationt.conversationState).to.be.equal(ConversationState.Bot); expect(conversationt.customerAddress).to.be.equal(CUSTOMER_ADDRESS); expect(conversationt.agentAddress).to.be.undefined; expect(eventHandlerSpies.unwatch.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.unwatch.success).to.have.been.calledOnce; expect(eventHandlerSpies.unwatch.failure).not.to.have.been.called; }); }); it('watch sets the conversation to wait and watch when in wait state', () => { eventMessage = new QueueEventMessage(CUSTOMER_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then(expectConvoIsInWaitAndWatchState) .then(() => { expect(eventHandlerSpies.queue.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.queue.success).to.have.been.calledOnce; expect(eventHandlerSpies.queue.failure).not.to.have.been.called; }); }); }); describe('wait/unwait', () => { beforeEach(() => { eventMessage = new QueueEventMessage(CUSTOMER_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((conversation: IConversation) => convo = conversation); }); it('wait sets the conversation state to wait when in bot state', () => { expect(convo.conversationState).to.be.equal(ConversationState.Wait); expect(convo.customerAddress).to.be.equal(CUSTOMER_ADDRESS); expect(convo.agentAddress).to.be.undefined; expect(eventHandlerSpies.queue.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.queue.success).to.have.been.calledOnce; expect(eventHandlerSpies.queue.failure).not.to.have.been.called; }); it('unwait sets the conversation state to bot when in a unwait state', () => { eventMessage = new DequeueEventMessage(CUSTOMER_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((convo: IConversation) => { expect(convo.conversationState).to.be.equal(ConversationState.Bot); expect(convo.customerAddress).to.be.equal(CUSTOMER_ADDRESS); expect(convo.agentAddress).to.be.undefined; expect(eventHandlerSpies.dequeue.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.dequeue.failure).not.to.have.been.called; }); }); it('wait sets the conversation to wait and watch when in watch state', () => { eventMessage = new WatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then(expectConvoIsInWaitAndWatchState) .then(() => { expect(eventHandlerSpies.watch.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.watch.success).to.have.been.calledOnce; expect(eventHandlerSpies.watch.failure).not.to.have.been.called; }); }); }); describe('conversation state in wait & watch', () => { beforeEach(() => { const watchStateEvent = new WatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); const waitStateEvent = new QueueEventMessage(CUSTOMER_ADDRESS); return sendMessageToBotAndGetConversationData(watchStateEvent) .then((convo: IConversation) => { expect(convo.conversationState).to.be.equal(ConversationState.Watch); expect(convo.agentAddress).to.deep.equal(AGENT_ADDRESS); expect(convo.customerAddress).to.deep.equal(CUSTOMER_ADDRESS); }) .then(() => sendMessageToBotAndGetConversationData(waitStateEvent)) .then(expectConvoIsInWaitAndWatchState) .then(() => { expect(eventHandlerSpies.watch.success).to.have.been.calledWith(bot, watchStateEvent); expect(eventHandlerSpies.watch.success).to.have.been.calledOnce; expect(eventHandlerSpies.watch.failure).not.to.have.been.called; expect(eventHandlerSpies.queue.success).to.have.been.calledWith(bot, waitStateEvent); expect(eventHandlerSpies.queue.success).to.have.been.calledOnce; expect(eventHandlerSpies.queue.failure).not.to.have.been.called; }); }); it('returns to wait with an unwatch event', () => { eventMessage = new UnwatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((convo: IConversation) => { expect(convo.conversationState).to.be.equal(ConversationState.Wait); expect(convo.agentAddress).to.be.undefined; expect(eventHandlerSpies.unwatch.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.unwatch.success).to.have.been.calledOnce; expect(eventHandlerSpies.unwatch.failure).not.to.have.been.called; }); }); it('returns to watch with an unwait (dequeue) event', () => { eventMessage = new DequeueEventMessage(CUSTOMER_ADDRESS); return sendMessageToBotAndGetConversationData(eventMessage) .then((convo: IConversation) => { expect(convo.conversationState).to.be.equal(ConversationState.Watch); expect(convo.agentAddress).to.deep.equal(AGENT_ADDRESS); expect(eventHandlerSpies.dequeue.success).to.have.been.calledWith(bot, eventMessage); expect(eventHandlerSpies.dequeue.success).to.have.been.calledOnce; expect(eventHandlerSpies.unwatch.failure).not.to.have.been.called; }); }); }); describe('conversation state unchanged error is thrown when', () => { let expectedErrorEvent: ErrorEventMessage; it('wait event message is sent to a conversation that is already waiting', () => { eventMessage = new QueueEventMessage(CUSTOMER_ADDRESS); expectedErrorEvent = new ErrorEventMessage(eventMessage, new ConversationStateUnchangedException('conversation was already in state wait')); return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(eventMessage) .sendMessageToBot(eventMessage, expectedErrorEvent) .runTest(); }); it('watch event message is sent to a conversation that is already in watch state', () => { eventMessage = new WatchEventMessage(CUSTOMER_ADDRESS, AGENT_ADDRESS); expectedErrorEvent = new ErrorEventMessage(eventMessage, new ConversationStateUnchangedException('')); return new BotTester(bot, CUSTOMER_ADDRESS) .sendMessageToBot(eventMessage) .sendMessageToBot(eventMessage, expectedErrorEvent) .runTest(); }); }); });