diff --git a/.vscode/launch.json b/.vscode/launch.json index e15595141..f98819ca7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,5 +35,20 @@ "cwd": "${workspaceFolder}/packages/app/main", "outFiles": [ "${workspaceRoot}/packages/app/main/app/server/*" ] }, + { + "type": "node", + "request": "launch", + "name": "Jest Current File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${relativeFile}", + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + } + } ] } diff --git a/packages/emulator/core/src/attachments/registerRoutes.spec.ts b/packages/emulator/core/src/attachments/registerRoutes.spec.ts new file mode 100644 index 000000000..4deb04b9d --- /dev/null +++ b/packages/emulator/core/src/attachments/registerRoutes.spec.ts @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; + +import registerRoutes from './registerRoutes'; +import getAttachment from './middleware/getAttachment'; +import getAttachmentInfo from './middleware/getAttachmentInfo'; + +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('./middleware/getAttachment', () => jest.fn(() => null)); +jest.mock('./middleware/getAttachmentInfo', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const server: any = { + get, + }; + const uses = []; + const emulator: any = {}; + registerRoutes(emulator, server, uses); + + expect(getFacility).toHaveBeenCalledWith('attachments'); + expect(get).toHaveBeenCalledWith( + '/v3/attachments/:attachmentId', + ...uses, + getFacility('attachments'), + getRouteName('getAttachmentInfo'), + getAttachmentInfo(emulator) + ); + expect(get).toHaveBeenCalledWith( + '/v3/attachments/:attachmentId/views/:viewId', + ...uses, + getFacility('attachments'), + getRouteName('getAttachment'), + getAttachment(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/botEmulator.spec.ts b/packages/emulator/core/src/botEmulator.spec.ts new file mode 100644 index 000000000..874db008f --- /dev/null +++ b/packages/emulator/core/src/botEmulator.spec.ts @@ -0,0 +1,145 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { GenericActivity, ILogItem } from '@bfemulator/sdk-shared'; + +import { BotEmulator } from './botEmulator'; +import ConsoleLogService from './facility/consoleLogService'; +import registerAttachmentRoutes from './attachments/registerRoutes'; +import registerBotStateRoutes from './botState/registerRoutes'; +import registerConversationRoutes from './conversations/registerRoutes'; +import registerDirectLineRoutes from './directLine/registerRoutes'; +import registerEmulatorRoutes from './emulator/registerRoutes'; +import registerSessionRoutes from './session/registerRoutes'; +import registerUserTokenRoutes from './userToken/registerRoutes'; +import stripEmptyBearerToken from './utils/stripEmptyBearerToken'; + +jest.mock('./attachments/registerRoutes', () => jest.fn(() => null)); +jest.mock('./botState/registerRoutes', () => jest.fn(() => null)); +jest.mock('./conversations/registerRoutes', () => jest.fn(() => null)); +jest.mock('./directLine/registerRoutes', () => jest.fn(() => null)); +jest.mock('./emulator/registerRoutes', () => jest.fn(() => null)); +jest.mock('./session/registerRoutes', () => jest.fn(() => null)); +jest.mock('./userToken/registerRoutes', () => jest.fn(() => null)); +jest.mock('./utils/stripEmptyBearerToken', () => jest.fn(() => null)); + +const mockAcceptParser = jest.fn(_acceptable => null); +const mockDateParser = jest.fn(() => null); +const mockQueryParser = jest.fn(() => null); +jest.mock('restify', () => ({ + plugins: { + get acceptParser() { + return mockAcceptParser; + }, + get dateParser() { + return mockDateParser; + }, + get queryParser() { + return mockQueryParser; + }, + }, +})); + +describe('BotEmulator', () => { + it('should instantiate itself properly', async () => { + const getServiceUrl = _url => Promise.resolve('serviceUrl'); + const customFetch = (_url, _options) => Promise.resolve(); + const customLogger = { + logActivity: (_conversationId: string, _activity: GenericActivity, _role: string) => 'activityLogged', + logMessage: (_conversationId: string, ..._items: ILogItem[]) => 'messageLogged', + logException: (_conversationId: string, _err: Error) => 'exceptionLogged', + }; + const customLogService = new ConsoleLogService(); + + // with logger + const options1 = { + fetch: customFetch, + loggerOrLogService: customLogger, + }; + const botEmulator1 = new BotEmulator(getServiceUrl, options1); + const serviceUrl = await botEmulator1.getServiceUrl(''); + + expect(serviceUrl).toBe('serviceUrl'); + expect(botEmulator1.options).toEqual({ ...options1, stateSizeLimitKB: 64 }); + expect(botEmulator1.facilities.attachments).not.toBeFalsy(); + expect(botEmulator1.facilities.botState).not.toBeFalsy(); + expect(botEmulator1.facilities.conversations).not.toBeFalsy(); + expect(botEmulator1.facilities.endpoints).not.toBeFalsy(); + expect(botEmulator1.facilities.logger).not.toBeFalsy(); + expect(botEmulator1.facilities.users).not.toBeFalsy(); + expect(botEmulator1.facilities.botState.stateSizeLimitKB).toBe(64); + expect(await botEmulator1.facilities.logger.logActivity('', null, '')).toBe('activityLogged'); + expect(await botEmulator1.facilities.logger.logException('', null)).toBe('exceptionLogged'); + expect(await botEmulator1.facilities.logger.logMessage('')).toBe('messageLogged'); + + // with log service + const options2 = { + fetch: customFetch, + loggerOrLogService: customLogService, + }; + const botEmulator2 = new BotEmulator(getServiceUrl, options2); + + expect((botEmulator2.facilities.logger as any).logService).toEqual(customLogService); + }); + + it('should mount routes onto a restify server', () => { + const getServiceUrl = _url => Promise.resolve('serviceUrl'); + const botEmulator = new BotEmulator(getServiceUrl); + const restifyServer = { acceptable: true }; + const mockUses = [ + mockAcceptParser(restifyServer.acceptable), + stripEmptyBearerToken(), + mockDateParser(), + mockQueryParser(), + ]; + mockAcceptParser.mockClear(); + (stripEmptyBearerToken as any).mockClear(); + mockDateParser.mockClear(); + mockQueryParser.mockClear(); + + const mountedEmulator = botEmulator.mount(restifyServer as any); + + expect(mountedEmulator).toEqual(botEmulator); + expect(mockAcceptParser).toHaveBeenCalledWith(restifyServer.acceptable); + expect(stripEmptyBearerToken).toHaveBeenCalled(); + expect(mockDateParser).toHaveBeenCalled(); + expect(mockQueryParser).toHaveBeenCalled(); + expect(registerAttachmentRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerBotStateRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerConversationRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerDirectLineRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerSessionRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerUserTokenRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + expect(registerEmulatorRoutes).toHaveBeenCalledWith(botEmulator, restifyServer, mockUses); + }); +}); diff --git a/packages/emulator/core/src/botState/registerRoutes.spec.ts b/packages/emulator/core/src/botState/registerRoutes.spec.ts new file mode 100644 index 000000000..9dd091986 --- /dev/null +++ b/packages/emulator/core/src/botState/registerRoutes.spec.ts @@ -0,0 +1,150 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; +import createBotFrameworkAuthenticationMiddleware from '../utils/botFrameworkAuthentication'; +import jsonBodyParser from '../utils/jsonBodyParser'; + +import registerRoutes from './registerRoutes'; +import deleteStateForUser from './middleware/deleteStateForUser'; +import createFetchBotDataMiddleware from './middleware/fetchBotData'; +import getConversationData from './middleware/getConversationData'; +import getPrivateConversationData from './middleware/getPrivateConversationData'; +import getUserData from './middleware/getUserData'; +import setConversationData from './middleware/setConversationData'; +import setPrivateConversationData from './middleware/setPrivateConversationData'; +import setUserData from './middleware/setUserData'; + +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('../utils/botFrameworkAuthentication', () => jest.fn(() => null)); +jest.mock('../utils/jsonBodyParser', () => jest.fn(() => null)); +jest.mock('./middleware/deleteStateForUser', () => jest.fn(() => null)); +jest.mock('./middleware/fetchBotData', () => jest.fn(() => null)); +jest.mock('./middleware/getConversationData', () => jest.fn(() => null)); +jest.mock('./middleware/getPrivateConversationData', () => jest.fn(() => null)); +jest.mock('./middleware/getUserData', () => jest.fn(() => null)); +jest.mock('./middleware/setConversationData', () => jest.fn(() => null)); +jest.mock('./middleware/setPrivateConversationData', () => jest.fn(() => null)); +jest.mock('./middleware/setUserData', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const post = jest.fn(() => null); + const del = jest.fn(() => null); + const server: any = { + get, + post, + del, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const verifyBotFramework = createBotFrameworkAuthenticationMiddleware(emulator.options.fetch); + const fetchBotDataMiddleware = createFetchBotDataMiddleware(emulator); + const facility = getFacility('state'); + registerRoutes(emulator, server, uses); + + expect(get).toHaveBeenCalledWith( + '/v3/botstate/:channelId/users/:userId', + ...uses, + verifyBotFramework, + fetchBotDataMiddleware, + facility, + getRouteName('getUserData'), + getUserData(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/botstate/:channelId/conversations/:conversationId', + ...uses, + verifyBotFramework, + fetchBotDataMiddleware, + facility, + getRouteName('getConversationData'), + getConversationData(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/botstate/:channelId/conversations/:conversationId/users/:userId', + ...uses, + verifyBotFramework, + fetchBotDataMiddleware, + facility, + getRouteName('getPrivateConversationData'), + getPrivateConversationData(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/botstate/:channelId/users/:userId', + ...uses, + verifyBotFramework, + jsonBodyParser(), + facility, + getRouteName('setUserData'), + setUserData(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/botstate/:channelId/conversations/:conversationId', + ...uses, + verifyBotFramework, + jsonBodyParser(), + facility, + getRouteName('setConversationData'), + setConversationData(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/botstate/:channelId/conversations/:conversationId/users/:userId', + ...uses, + verifyBotFramework, + jsonBodyParser(), + facility, + getRouteName('setPrivateConversationData'), + setPrivateConversationData(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/v3/botstate/:channelId/users/:userId', + ...uses, + verifyBotFramework, + facility, + getRouteName('deleteStateForUser'), + deleteStateForUser(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/conversations/registerRoutes.spec.ts b/packages/emulator/core/src/conversations/registerRoutes.spec.ts new file mode 100644 index 000000000..e7988be3e --- /dev/null +++ b/packages/emulator/core/src/conversations/registerRoutes.spec.ts @@ -0,0 +1,186 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; +import createBotFrameworkAuthenticationMiddleware from '../utils/botFrameworkAuthentication'; +import createJsonBodyParser from '../utils/jsonBodyParser'; + +import registerRoutes from './registerRoutes'; +import createConversation from './middleware/createConversation'; +import deleteActivity from './middleware/deleteActivity'; +import createFetchConversationMiddleware from './middleware/fetchConversation'; +import getActivityMembers from './middleware/getActivityMembers'; +import getBotEndpoint from './middleware/getBotEndpoint'; +import getConversationMembers from './middleware/getConversationMembers'; +import replyToActivity from './middleware/replyToActivity'; +import sendActivityToConversation from './middleware/sendActivityToConversation'; +import sendHistoryToConversation from './middleware/sendHistoryToConversation'; +import updateActivity from './middleware/updateActivity'; +import uploadAttachment from './middleware/uploadAttachment'; + +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('../utils/botFrameworkAuthentication', () => jest.fn(() => null)); +jest.mock('../utils/jsonBodyParser', () => jest.fn(() => null)); +jest.mock('./middleware/createConversation', () => jest.fn(() => null)); +jest.mock('./middleware/deleteActivity', () => jest.fn(() => null)); +jest.mock('./middleware/fetchConversation', () => jest.fn(() => null)); +jest.mock('./middleware/getActivityMembers', () => jest.fn(() => null)); +jest.mock('./middleware/getBotEndpoint', () => jest.fn(() => null)); +jest.mock('./middleware/getConversationMembers', () => jest.fn(() => null)); +jest.mock('./middleware/replyToActivity', () => jest.fn(() => null)); +jest.mock('./middleware/sendActivityToConversation', () => jest.fn(() => null)); +jest.mock('./middleware/sendHistoryToConversation', () => jest.fn(() => null)); +jest.mock('./middleware/updateActivity', () => jest.fn(() => null)); +jest.mock('./middleware/uploadAttachment', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const post = jest.fn(() => null); + const del = jest.fn(() => null); + const put = jest.fn(() => null); + const server: any = { + get, + post, + del, + put, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const verifyBotFramework = createBotFrameworkAuthenticationMiddleware(emulator.options.fetch); + const botEndpoint = getBotEndpoint(emulator); + const facility = getFacility('conversations'); + const jsonBodyParser = createJsonBodyParser(); + const fetchConversation = createFetchConversationMiddleware(emulator); + registerRoutes(emulator, server, uses); + + expect(post).toHaveBeenCalledWith( + '/v3/conversations', + ...uses, + verifyBotFramework, + jsonBodyParser, + botEndpoint, + facility, + getRouteName('createConversation'), + createConversation(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities', + ...uses, + verifyBotFramework, + jsonBodyParser, + fetchConversation, + facility, + getRouteName('sendToConversation'), + sendActivityToConversation(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities/history', + ...uses, + verifyBotFramework, + jsonBodyParser, + fetchConversation, + facility, + getRouteName('sendToConversation'), + sendHistoryToConversation(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities/:activityId', + ...uses, + verifyBotFramework, + jsonBodyParser, + fetchConversation, + facility, + getRouteName('replyToActivity'), + replyToActivity(emulator) + ); + + expect(put).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities/:activityId', + ...uses, + verifyBotFramework, + jsonBodyParser, + fetchConversation, + facility, + getRouteName('updateActivity'), + updateActivity(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities/:activityId', + ...uses, + verifyBotFramework, + fetchConversation, + facility, + getRouteName('deleteActivity'), + deleteActivity(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/members', + ...uses, + verifyBotFramework, + fetchConversation, + facility, + getRouteName('getConversationMembers'), + getConversationMembers(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/activities/:activityId/members', + ...uses, + verifyBotFramework, + fetchConversation, + facility, + getRouteName('getActivityMembers'), + getActivityMembers(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/conversations/:conversationId/attachments', + ...uses, + verifyBotFramework, + jsonBodyParser, + facility, + getRouteName('uploadAttachment'), + uploadAttachment(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/directLine/registerRoutes.spec.ts b/packages/emulator/core/src/directLine/registerRoutes.spec.ts new file mode 100644 index 000000000..6bf5bdaa7 --- /dev/null +++ b/packages/emulator/core/src/directLine/registerRoutes.spec.ts @@ -0,0 +1,143 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getBotEndpoint from '../middleware/getBotEndpoint'; +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; +import createJsonBodyParserMiddleware from '../utils/jsonBodyParser'; + +import getActivities from './middleware/getActivities'; +import getConversation from './middleware/getConversation'; +import options from './middleware/options'; +import postActivity from './middleware/postActivity'; +import reconnectToConversation from './middleware/reconnectToConversation'; +import startConversation from './middleware/startConversation'; +import stream from './middleware/stream'; +import upload from './middleware/upload'; +import registerRoutes from './registerRoutes'; + +jest.mock('../middleware/getBotEndpoint', () => jest.fn(() => null)); +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('../utils/jsonBodyParser', () => jest.fn(() => null)); +jest.mock('./middleware/getActivities', () => jest.fn(() => null)); +jest.mock('./middleware/getConversation', () => jest.fn(() => null)); +jest.mock('./middleware/options', () => jest.fn(() => null)); +jest.mock('./middleware/postActivity', () => jest.fn(() => null)); +jest.mock('./middleware/reconnectToConversation', () => jest.fn(() => null)); +jest.mock('./middleware/startConversation', () => jest.fn(() => null)); +jest.mock('./middleware/stream', () => jest.fn(() => null)); +jest.mock('./middleware/upload', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const post = jest.fn(() => null); + const opts = jest.fn(() => null); + const server: any = { + get, + post, + opts, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const jsonBodyParser = createJsonBodyParserMiddleware(); + const botEndpoint = getBotEndpoint(emulator); + const conversation = getConversation(emulator); + const facility = getFacility('directline'); + registerRoutes(emulator, server, uses); + + expect(opts).toHaveBeenCalledWith('/v3/directline', ...uses, facility, getRouteName('options'), options(emulator)); + + expect(post).toHaveBeenCalledWith( + '/v3/directline/conversations', + ...uses, + botEndpoint, + jsonBodyParser, + facility, + getRouteName('startConversation'), + startConversation(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/directline/conversations/:conversationId', + ...uses, + botEndpoint, + conversation, + facility, + getRouteName('reconnectToConversation'), + reconnectToConversation(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/directline/conversations/:conversationId/activities', + ...uses, + botEndpoint, + conversation, + facility, + getRouteName('getActivities'), + getActivities(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/directline/conversations/:conversationId/activities', + ...uses, + jsonBodyParser, + botEndpoint, + conversation, + facility, + getRouteName('postActivity'), + postActivity(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/v3/directline/conversations/:conversationId/upload', + ...uses, + botEndpoint, + conversation, + facility, + getRouteName('upload'), + upload(emulator) + ); + + expect(get).toHaveBeenCalledWith( + '/v3/directline/conversations/:conversationId/stream', + ...uses, + facility, + getRouteName('stream'), + stream(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/emulator/registerRoutes.spec.ts b/packages/emulator/core/src/emulator/registerRoutes.spec.ts new file mode 100644 index 000000000..955e3e9a3 --- /dev/null +++ b/packages/emulator/core/src/emulator/registerRoutes.spec.ts @@ -0,0 +1,189 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; +import createJsonBodyParserMiddleware from '../utils/jsonBodyParser'; + +import addUsers from './middleware/addUsers'; +import contactAdded from './middleware/contactAdded'; +import contactRemoved from './middleware/contactRemoved'; +import deleteUserData from './middleware/deleteUserData'; +import createFetchConversationMiddleware from './middleware/fetchConversation'; +import getUsers from './middleware/getUsers'; +import paymentComplete from './middleware/paymentComplete'; +import ping from './middleware/ping'; +import removeUsers from './middleware/removeUsers'; +import sendTokenResponse from './middleware/sendTokenResponse'; +import typing from './middleware/typing'; +import updateShippingAddress from './middleware/updateShippingAddress'; +import updateShippingOption from './middleware/updateShippingOption'; +import registerRoutes from './registerRoutes'; + +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('../utils/jsonBodyParser', () => jest.fn(() => null)); +jest.mock('./middleware/addUsers', () => jest.fn(() => null)); +jest.mock('./middleware/contactAdded', () => jest.fn(() => null)); +jest.mock('./middleware/contactRemoved', () => jest.fn(() => null)); +jest.mock('./middleware/deleteUserData', () => jest.fn(() => null)); +jest.mock('./middleware/fetchConversation', () => jest.fn(() => null)); +jest.mock('./middleware/getUsers', () => jest.fn(() => null)); +jest.mock('./middleware/paymentComplete', () => jest.fn(() => null)); +jest.mock('./middleware/ping', () => jest.fn(() => null)); +jest.mock('./middleware/removeUsers', () => jest.fn(() => null)); +jest.mock('./middleware/sendTokenResponse', () => jest.fn(() => null)); +jest.mock('./middleware/typing', () => jest.fn(() => null)); +jest.mock('./middleware/updateShippingAddress', () => jest.fn(() => null)); +jest.mock('./middleware/updateShippingOption', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const post = jest.fn(() => null); + const del = jest.fn(() => null); + const server: any = { + get, + post, + del, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const fetchConversation = createFetchConversationMiddleware(emulator); + const jsonBodyParser = createJsonBodyParserMiddleware(); + const facility = getFacility('emulator'); + registerRoutes(emulator, server, uses); + + expect(get).toHaveBeenCalledWith( + '/emulator/:conversationId/users', + fetchConversation, + facility, + getRouteName('getUsers'), + getUsers(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/users', + jsonBodyParser, + fetchConversation, + facility, + getRouteName('addUsers'), + addUsers(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/emulator/:conversationId/users', + fetchConversation, + facility, + getRouteName('removeUsers'), + removeUsers(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/contacts', + fetchConversation, + facility, + getRouteName('contactAdded'), + contactAdded(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/emulator/:conversationId/contacts', + fetchConversation, + facility, + getRouteName('contactRemoved'), + contactRemoved(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/typing', + fetchConversation, + facility, + getRouteName('typing'), + typing(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/ping', + fetchConversation, + facility, + getRouteName('ping'), + ping(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/emulator/:conversationId/userdata', + fetchConversation, + facility, + getRouteName('deleteUserData'), + deleteUserData(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/invoke/updateShippingAddress', + jsonBodyParser, + fetchConversation, + facility, + getRouteName('updateShippingAddress'), + updateShippingAddress(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/invoke/updateShippingOption', + jsonBodyParser, + fetchConversation, + facility, + getRouteName('updateShippingOption'), + updateShippingOption(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/invoke/paymentComplete', + jsonBodyParser, + fetchConversation, + facility, + getRouteName('paymentComplete'), + paymentComplete(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/emulator/:conversationId/invoke/sendTokenResponse', + jsonBodyParser, + facility, + getRouteName('sendTokenResponse'), + sendTokenResponse(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/facility/attachments.spec.ts b/packages/emulator/core/src/facility/attachments.spec.ts new file mode 100644 index 000000000..d90f44144 --- /dev/null +++ b/packages/emulator/core/src/facility/attachments.spec.ts @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Attachments from './attachments'; + +jest.mock('../utils/uniqueId', () => jest.fn(() => '1234')); +jest.mock('../utils/createResponse/apiException', () => jest.fn(() => 'I am an error!')); + +describe('Attachments', () => { + const attachments = new Attachments(); + + it('should upload and get attachments', () => { + const attachmentData: any = { + type: 'someType', + originalBase64: [0x00], + data: 123, + }; + const attachmentId = attachments.uploadAttachment(attachmentData); + expect(attachmentId).toBe('1234'); + + const retrievedAttachment = attachments.getAttachmentData(attachmentId); + expect(retrievedAttachment).toEqual({ ...attachmentData, id: attachmentId }); + }); + + it('should throw when no attachment type is supplied', () => { + const attachmentData: any = { + data: 123, + }; + expect(() => attachments.uploadAttachment(attachmentData)).toThrow('I am an error!'); + }); + + it('should throw when no originalBase64 byte array is supplied', () => { + const attachmentData: any = { + data: 123, + type: 'someType', + }; + expect(() => attachments.uploadAttachment(attachmentData)).toThrow('I am an error!'); + }); +}); diff --git a/packages/emulator/core/src/facility/botDataKey.spec.ts b/packages/emulator/core/src/facility/botDataKey.spec.ts new file mode 100644 index 000000000..35eda5b35 --- /dev/null +++ b/packages/emulator/core/src/facility/botDataKey.spec.ts @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import botDataKey from './botDataKey'; + +describe('botDataKey', () => { + it('should return a bot data key', () => { + let key = botDataKey('', '', ''); + expect(key).toBe('*!*!*'); + + key = botDataKey('channelId', 'convoId', 'userId'); + expect(key).toBe('channelId!convoId!userId'); + }); +}); diff --git a/packages/emulator/core/src/facility/botEndpoint.spec.ts b/packages/emulator/core/src/facility/botEndpoint.spec.ts new file mode 100644 index 000000000..e1690cde8 --- /dev/null +++ b/packages/emulator/core/src/facility/botEndpoint.spec.ts @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { URLSearchParams } from 'url'; + +import { authentication, usGovernmentAuthentication } from '../authEndpoints'; + +import BotEndpoint from './botEndpoint'; + +describe('BotEndpoint', () => { + it('should return the speech token if it already exists', async () => { + const endpoint = new BotEndpoint(); + endpoint.speechToken = 'someToken'; + const refresh = false; + const token = await endpoint.getSpeechToken(refresh); + expect(token).toBe('someToken'); + }); + + it('should throw if there is no msa app id or password', async () => { + const endpoint = new BotEndpoint(); + try { + await endpoint.getSpeechToken(); + } catch (e) { + expect(e).toEqual(new Error('bot must have Microsoft App ID and password')); + } + }); + + it('should return a speech token', async () => { + /* eslint-disable typescript/camelcase */ + const mockResponse = { + json: async () => Promise.resolve({ access_Token: 'someSpeechToken' }), + status: 200, + }; + const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse)); + const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw'); + endpoint.fetchWithAuth = mockFetchWithAuth; + const token = await endpoint.getSpeechToken(); + expect(token).toBe('someSpeechToken'); + }); + + it('should throw if there is no access_Token in the response', async () => { + /* eslint-disable typescript/camelcase */ + + // with error in response body + let mockResponse: any = { + json: async () => Promise.resolve({ error: 'someError' }), + status: 200, + }; + const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse)); + const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw'); + endpoint.fetchWithAuth = mockFetchWithAuth; + try { + await endpoint.getSpeechToken(); + } catch (e) { + expect(e).toEqual(new Error('someError')); + } + + // with no error in response body + mockResponse = { + json: async () => Promise.resolve({}), + status: 200, + }; + try { + await endpoint.getSpeechToken(); + } catch (e) { + expect(e).toEqual(new Error('could not retrieve speech token')); + } + }); + + it('should throw if the call to the speech service returns a 401', async () => { + /* eslint-disable typescript/camelcase */ + const mockResponse: any = { + status: 401, + }; + const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse)); + const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw'); + endpoint.fetchWithAuth = mockFetchWithAuth; + try { + await endpoint.getSpeechToken(); + } catch (e) { + expect(e).toEqual(new Error('not authorized to use Cognitive Services Speech API')); + } + }); + + it('should throw if the call to the speech service returns a non-200', async () => { + /* eslint-disable typescript/camelcase */ + const mockResponse: any = { + status: 500, + }; + const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse)); + const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw'); + endpoint.fetchWithAuth = mockFetchWithAuth; + try { + await endpoint.getSpeechToken(); + } catch (e) { + expect(e).toEqual(new Error('cannot retrieve speech token')); + } + }); + + it('should fetch with auth', async () => { + const endpoint = new BotEndpoint(); + endpoint.msaAppId = 'someAppId'; + const mockGetAccessToken = jest.fn(() => Promise.resolve('someAccessToken')); + (endpoint as any).getAccessToken = mockGetAccessToken; + const mockResponse = 'I am a response!'; + const mockFetch = jest.fn(() => Promise.resolve(mockResponse)); + (endpoint as any)._options = { fetch: mockFetch }; + const response = await endpoint.fetchWithAuth('someUrl'); + + expect(response).toBe('I am a response!'); + expect(mockGetAccessToken).toHaveBeenCalledWith(false); + expect(mockFetch).toHaveBeenCalledWith('someUrl', { headers: { Authorization: 'Bearer someAccessToken' } }); + }); + + it('should retry fetching with a refreshed auth token if the fetch returns a 401', async () => { + const endpoint = new BotEndpoint(); + endpoint.msaAppId = 'someAppId'; + const mockGetAccessToken = jest.fn(() => Promise.resolve('someAccessToken')); + (endpoint as any).getAccessToken = mockGetAccessToken; + const mockResponse = 'I am a response!'; + const mockFetch = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({ status: 401 })) + .mockImplementationOnce(() => Promise.resolve(mockResponse)); + (endpoint as any)._options = { fetch: mockFetch }; + const response = await endpoint.fetchWithAuth('someUrl'); + + expect(response).toBe('I am a response!'); + expect(mockGetAccessToken).toHaveBeenCalledTimes(2); + expect(mockGetAccessToken).toHaveBeenCalledWith(true); // forceRefresh will be true on second attempt + }); + + it('should retry fetching with a refreshed auth token if the fetch returns a 403', async () => { + const endpoint = new BotEndpoint(); + endpoint.msaAppId = 'someAppId'; + const mockGetAccessToken = jest.fn(() => Promise.resolve('someAccessToken')); + (endpoint as any).getAccessToken = mockGetAccessToken; + const mockResponse = 'I am a response!'; + const mockFetch = jest + .fn() + .mockImplementationOnce(() => Promise.resolve({ status: 403 })) + .mockImplementationOnce(() => Promise.resolve(mockResponse)); + (endpoint as any)._options = { fetch: mockFetch }; + const response = await endpoint.fetchWithAuth('someUrl'); + + expect(response).toBe('I am a response!'); + expect(mockGetAccessToken).toHaveBeenCalledTimes(2); + expect(mockGetAccessToken).toHaveBeenCalledWith(true); // forceRefresh will be true on second attempt + }); + + it('should return the access token if it already exists and has not expired yet', async () => { + const endpoint = new BotEndpoint(); + const msaAppId = 'someAppId'; + const msaPw = 'someAppPw'; + endpoint.msaAppId = msaAppId; + endpoint.msaPassword = msaPw; + endpoint.use10Tokens = false; + endpoint.channelService = undefined; + // ensure that the token won't be expired + const tokenRefreshTime = 5 * 60 * 1000; + const accessTokenExpires = Date.now() * 2 + tokenRefreshTime; + endpoint.accessTokenExpires = accessTokenExpires; + // using non-v1.0 token & standard endpoint + const mockOauthResponse = { access_token: 'I am an access token!', expires_in: 10 }; + const mockResponse = { json: jest.fn(() => Promise.resolve(mockOauthResponse)), status: 200 }; + const mockFetch = jest.fn(() => Promise.resolve(mockResponse)); + (endpoint as any)._options = { fetch: mockFetch }; + let response = await (endpoint as any).getAccessToken(); + + expect(response).toBe('I am an access token!'); + expect(endpoint.accessToken).toBe('I am an access token!'); + expect(endpoint.accessTokenExpires).not.toEqual(accessTokenExpires); + expect(endpoint.accessTokenExpires).toEqual(jasmine.any(Number)); + expect(mockFetch).toHaveBeenCalledWith(authentication.tokenEndpoint, { + method: 'POST', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: msaAppId, + client_secret: msaPw, + scope: `${msaAppId}/.default`, + } as { [key: string]: string }).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + // using v1.0 token & government endpoint + endpoint.use10Tokens = true; + endpoint.channelService = usGovernmentAuthentication.channelService; + response = await (endpoint as any).getAccessToken(); + + expect(response).toBe('I am an access token!'); + expect(endpoint.accessToken).toBe('I am an access token!'); + expect(endpoint.accessTokenExpires).not.toEqual(accessTokenExpires); + expect(endpoint.accessTokenExpires).toEqual(jasmine.any(Number)); + expect(mockFetch).toHaveBeenCalledWith(usGovernmentAuthentication.tokenEndpoint, { + method: 'POST', + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: msaAppId, + client_secret: msaPw, + scope: `${msaAppId}/.default`, + atver: '1', + } as { [key: string]: string }).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + }); + + it('should throw when requesting an access returns a bad response', async () => { + const endpoint = new BotEndpoint(); + const msaAppId = 'someAppId'; + const msaPw = 'someAppPw'; + endpoint.msaAppId = msaAppId; + endpoint.msaPassword = msaPw; + endpoint.use10Tokens = false; + endpoint.channelService = undefined; + // ensure that the token won't be expired + const tokenRefreshTime = 5 * 60 * 1000; + const accessTokenExpires = Date.now() * 2 + tokenRefreshTime; + endpoint.accessTokenExpires = accessTokenExpires; + const mockResponse = { status: 404 }; + const mockFetch = jest.fn(() => Promise.resolve(mockResponse)); + (endpoint as any)._options = { fetch: mockFetch }; + + try { + const response = await (endpoint as any).getAccessToken(); + } catch (e) { + expect(e).toEqual(new Error('Refresh access token failed with status code: 404')); + } + }); +}); diff --git a/packages/emulator/core/src/facility/botState.spec.ts b/packages/emulator/core/src/facility/botState.spec.ts new file mode 100644 index 000000000..13a3d5702 --- /dev/null +++ b/packages/emulator/core/src/facility/botState.spec.ts @@ -0,0 +1,176 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { ErrorCodes } from '@bfemulator/sdk-shared'; +import * as HttpStatus from 'http-status-codes'; + +import createAPIException from '../utils/createResponse/apiException'; + +import BotState from './botState'; +import botDataKey from './botDataKey'; + +let mockApproximatedObjectSize; +jest.mock('../utils/approximateObjectSize', () => jest.fn(() => mockApproximatedObjectSize)); + +describe('botState', () => { + beforeEach(() => { + mockApproximatedObjectSize = 64; + }); + + it('should get bot data when the record exists', () => { + const channelId = 'channel1'; + const convoId = 'convo1'; + const userId = 'user1'; + const dataKey = botDataKey(channelId, convoId, userId); + const botData = { data: 'I am bot data!' }; + const mockLogDeprecationWarning = jest.fn(() => null); + const botState = new BotState(null, null); + (botState as any).botDataStore = { [dataKey]: botData }; + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + + const data = botState.getBotData(channelId, convoId, userId); + + expect(data).toBe(botData); + expect(mockLogDeprecationWarning).toHaveBeenCalledWith(convoId); + }); + + it('should get null bot data when the record does not exist', () => { + const mockLogDeprecationWarning = jest.fn(() => null); + const botState = new BotState(null, null); + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + + const data = botState.getBotData('channel1', 'convo1', 'user1'); + + expect(data).toEqual({ data: null, eTag: '*' }); + }); + + it('should set bot data', () => { + const channelId = 'channel1'; + const convoId = 'convo1'; + const userId = 'user1'; + const dataKey = botDataKey(channelId, convoId, userId); + const mockLogDeprecationWarning = jest.fn(() => null); + const botState = new BotState(null, null); + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + const botData: any = { data: 'I am bot data!' }; + + const data = botState.setBotData(channelId, convoId, userId, botData); + + expect(mockLogDeprecationWarning).toHaveBeenCalledWith(convoId); + expect(data.eTag).toEqual(jasmine.any(String)); + expect(data.data).toBe(botData.data); + expect((botState as any).botDataStore[dataKey]).toBe(data); + }); + + it('should delete the record when trying to set bot data with invalid data', () => { + const channelId = 'channel1'; + const convoId = 'convo1'; + const userId = 'user1'; + const dataKey = botDataKey(channelId, convoId, userId); + const mockLogDeprecationWarning = jest.fn(() => null); + const botData: any = { data: null }; + const botState = new BotState(null, null); + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + (botState as any).botDataStore[dataKey] = botData; + + expect((botState as any).botDataStore[dataKey]).toBe(botData); + + const data = botState.setBotData(channelId, convoId, userId, botData); + + expect(mockLogDeprecationWarning).toHaveBeenCalledWith(convoId); + expect(data.eTag).toBe('*'); + expect(data.data).toBe(null); + expect((botState as any).botDataStore[dataKey]).toBe(undefined); + }); + + it('should throw when the bot data etags are mismatched', () => { + const channelId = 'channel1'; + const convoId = 'convo1'; + const userId = 'user1'; + const dataKey = botDataKey(channelId, convoId, userId); + const mockLogDeprecationWarning = jest.fn(() => null); + const oldBotData: any = { data: 'I am bot data!', eTag: 'Modified on Thursday' }; + const newBotData: any = { data: 'I am bot data!', eTag: 'Modified on Friday' }; + const botState = new BotState(null, null); + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + (botState as any).botDataStore[dataKey] = oldBotData; + + try { + botState.setBotData(channelId, convoId, userId, newBotData); + } catch (e) { + expect(e).toEqual( + createAPIException(HttpStatus.PRECONDITION_FAILED, ErrorCodes.BadArgument, 'The data is changed') + ); + } + }); + + it('should throw when the bot data is larger than the size limit', () => { + const mockLogDeprecationWarning = jest.fn(() => null); + const botState = new BotState(null, null); + mockApproximatedObjectSize = 9999; // ensure that the size exceeds the limit + botState.stateSizeLimitKB = 1; + (botState as any).logBotStateApiDeprecationWarning = mockLogDeprecationWarning; + const botData: any = { data: 'I am bot data!' }; + + try { + botState.setBotData('', '', '', botData); + } catch (e) { + expect(e).toEqual( + createAPIException( + HttpStatus.BAD_REQUEST, + ErrorCodes.MessageSizeTooBig, + 'State size exceeded configured limit.' + ) + ); + } + }); + + it('should delete bot data', () => { + const userId = '1234'; + const dataKey1 = botDataKey('channel1', 'convo1', userId); + const dataKey2 = botDataKey('channel1', 'convo2', userId); + const botData1 = { data: 'I am some bot data!' }; + const botData2 = { data: 'I am some other bot data!' }; + const botState = new BotState(null, null); + (botState as any).botDataStore = { + [dataKey1]: botData1, + [dataKey2]: botData2, + }; + + expect(Object.keys((botState as any).botDataStore)).toHaveLength(2); + + botState.deleteBotData(userId); + + expect(Object.keys((botState as any).botDataStore)).toHaveLength(0); + }); +}); diff --git a/packages/emulator/core/src/facility/consoleLogService.spec.ts b/packages/emulator/core/src/facility/consoleLogService.spec.ts new file mode 100644 index 000000000..1ab90f3d7 --- /dev/null +++ b/packages/emulator/core/src/facility/consoleLogService.spec.ts @@ -0,0 +1,101 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { LogLevel } from '@bfemulator/sdk-shared'; +import log from 'npmlog'; + +import ConsoleLogService from './consoleLogService'; + +jest.mock('npmlog', () => ({ + error: jest.fn(() => null), + info: jest.fn(() => null), + warn: jest.fn(() => null), + silly: jest.fn(() => null), +})); + +describe('ConsoleLogService', () => { + const errorSpy = jest.spyOn(log, 'error'); + const infoSpy = jest.spyOn(log, 'info'); + const warnSpy = jest.spyOn(log, 'warn'); + const sillySpy = jest.spyOn(log, 'silly'); + + beforeEach(() => { + errorSpy.mockClear(); + infoSpy.mockClear(); + warnSpy.mockClear(); + sillySpy.mockClear(); + }); + + it('should log items to chat with a conversation id', () => { + const conversationId = 'convo1'; + const messages: any = [ + { type: 'text', payload: { level: LogLevel.Debug, text: 'msg1' } }, + { type: 'text', payload: { level: LogLevel.Error, text: 'msg2' } }, + { type: 'text', payload: { level: LogLevel.Info, text: 'msg3' } }, + { type: 'text', payload: { level: LogLevel.Warn, text: 'msg4' } }, + { type: undefined }, + ]; + const consoleLogService = new ConsoleLogService(); + consoleLogService.logToChat(conversationId, ...messages); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith(conversationId, 'msg2'); + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith(conversationId, 'msg3'); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(conversationId, 'msg4'); + expect(sillySpy).toHaveBeenCalledTimes(1); + expect(sillySpy).toHaveBeenCalledWith(conversationId, 'msg1'); + }); + + it('should log items to chat with no conversation id', () => { + const messages: any = [ + { type: 'text', payload: { level: LogLevel.Debug, text: 'msg1' } }, + { type: 'text', payload: { level: LogLevel.Error, text: 'msg2' } }, + { type: 'text', payload: { level: LogLevel.Info, text: 'msg3' } }, + { type: 'text', payload: { level: LogLevel.Warn, text: 'msg4' } }, + { type: undefined }, + ]; + const consoleLogService = new ConsoleLogService(); + consoleLogService.logToChat(undefined, ...messages); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('', 'msg2'); + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith('', 'msg3'); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith('', 'msg4'); + expect(sillySpy).toHaveBeenCalledTimes(1); + expect(sillySpy).toHaveBeenCalledWith('', 'msg1'); + }); +}); diff --git a/packages/emulator/core/src/facility/conversationSet.spec.ts b/packages/emulator/core/src/facility/conversationSet.spec.ts index f1f1a5cc5..9483b57f6 100644 --- a/packages/emulator/core/src/facility/conversationSet.spec.ts +++ b/packages/emulator/core/src/facility/conversationSet.spec.ts @@ -1,7 +1,41 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + import ConversationSet from './conversationSet'; describe('The conversationSet', () => { let conversationSet: ConversationSet; + beforeEach(() => { conversationSet = new ConversationSet(); }); diff --git a/packages/emulator/core/src/facility/endpointSet.spec.ts b/packages/emulator/core/src/facility/endpointSet.spec.ts new file mode 100644 index 000000000..e675f6a87 --- /dev/null +++ b/packages/emulator/core/src/facility/endpointSet.spec.ts @@ -0,0 +1,200 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Endpoints from './endpointSet'; +import BotEndpoint from './botEndpoint'; + +const mockId = '1234'; +jest.mock('../utils/uniqueId', () => jest.fn(() => mockId)); +let mockDecodedToken; +jest.mock('on-error-resume-next', () => jest.fn(() => mockDecodedToken)); + +describe('Endpoints', () => { + let endpoints: Endpoints; + const mockFetch = jest.fn(() => null); + + beforeEach(() => { + const options: any = { + fetch: mockFetch, + }; + endpoints = new Endpoints(options); + mockDecodedToken = { endpointId: 'someOtherId' }; + }); + + it('should push an endpoint', () => { + const id = 'someId'; + const botEndpoint = { + botId: 'someBotId', + botUrl: 'someBotUrl', + msaAppId: 'someMsaAppId', + msaPassword: 'someMsaPassword', + use10Tokens: false, + channelService: undefined, + }; + // with an id + let pushedEndpoint = endpoints.push(id, botEndpoint); + + expect(pushedEndpoint).toEqual( + new BotEndpoint( + id, + botEndpoint.botId, + botEndpoint.botUrl, + botEndpoint.msaAppId, + botEndpoint.msaPassword, + botEndpoint.use10Tokens, + botEndpoint.channelService, + { + fetch: mockFetch, + } + ) + ); + expect((endpoints as any)._endpoints[id]).toEqual(pushedEndpoint); + + // with no id + pushedEndpoint = endpoints.push('', botEndpoint); + + expect(pushedEndpoint).toEqual( + new BotEndpoint( + botEndpoint.botUrl, // url will be used in place of id + botEndpoint.botId, + botEndpoint.botUrl, + botEndpoint.msaAppId, + botEndpoint.msaPassword, + botEndpoint.use10Tokens, + botEndpoint.channelService, + { + fetch: mockFetch, + } + ) + ); + expect((endpoints as any)._endpoints[botEndpoint.botUrl]).toEqual(pushedEndpoint); + + // with no id or url + botEndpoint.botUrl = ''; + pushedEndpoint = endpoints.push('', botEndpoint); + + expect(pushedEndpoint).toEqual( + new BotEndpoint( + mockId, // unique id will be used in place of id + botEndpoint.botId, + botEndpoint.botUrl, + botEndpoint.msaAppId, + botEndpoint.msaPassword, + botEndpoint.use10Tokens, + botEndpoint.channelService, + { + fetch: mockFetch, + } + ) + ); + expect((endpoints as any)._endpoints[mockId]).toEqual(pushedEndpoint); + }); + + it('should reset the endpoints store', () => { + const endpoint = { botUrl: 'http://localhost:3978/api/messages' }; + (endpoints as any)._endpoints['someId'] = endpoint; + + expect((endpoints as any)._endpoints['someId']).toBe(endpoint); + endpoints.reset(); + expect((endpoints as any)._endpoints['someId']).toBe(undefined); + }); + + it('should get a default endpoint', () => { + const endpoint1 = { botUrl: 'http://localhost:3978/api/messages' }; + const endpoint2 = { botUrl: 'https://mybot.azurewebsites.net/api/messages' }; + (endpoints as any)._endpoints['id1'] = endpoint1; + (endpoints as any)._endpoints['id2'] = endpoint2; + + expect(endpoints.getDefault()).toBe(endpoint1); + }); + + it('should get an endpoint if it is already in the store indexed by id', () => { + const endpoint = { botUrl: 'http://localhost:3978/api/messages' }; + (endpoints as any)._endpoints['id1'] = endpoint; + + expect(endpoints.get('id1')).toBe(endpoint); + }); + + it('should get an endpoint if it is already in the store, but the id is encoded in base64 ', () => { + const endpoint = { botUrl: 'http://localhost:3978/api/messages' }; + (endpoints as any)._endpoints['someOtherId'] = endpoint; + + expect(endpoints.get('id1')).toBe(endpoint); + }); + + it('should return null if the endpoint is not in the store', () => { + mockDecodedToken = {}; + + expect(endpoints.get('id1')).toBe(null); + }); + + it('should get an endpoint by MSA app id', () => { + const endpoint = { botUrl: 'http://localhost:3978/api/messages', msaAppId: 'someMsaAppId' }; + (endpoints as any)._endpoints['someId'] = endpoint; + + expect(endpoints.getByAppId('someMsaAppId')).toBe(endpoint); + }); + + it('should get all endpoints', () => { + const endpoint1 = { + botId: 'botId1', + botUrl: 'botUrl1', + msaAppId: 'msaAppId1', + msaPassword: 'msaPassword1', + use10Tokens: 'use10Tokens1', + }; + const endpoint2 = { + botId: 'botId2', + botUrl: 'botUrl2', + msaAppId: 'msaAppId2', + msaPassword: 'msaPassword2', + use10Tokens: 'use10Tokens2', + }; + const endpoint3 = { + botId: 'botId3', + botUrl: 'botUrl3', + msaAppId: 'msaAppId3', + msaPassword: 'msaPassword3', + use10Tokens: 'use10Tokens3', + }; + (endpoints as any)._endpoints['id1'] = endpoint1; + (endpoints as any)._endpoints['id2'] = endpoint2; + (endpoints as any)._endpoints['id3'] = endpoint3; + + expect(endpoints.getAll()).toEqual({ + id1: endpoint1, + id2: endpoint2, + id3: endpoint3, + }); + }); +}); diff --git a/packages/emulator/core/src/facility/endpointSet.ts b/packages/emulator/core/src/facility/endpointSet.ts index 7a94de5f7..561e654eb 100644 --- a/packages/emulator/core/src/facility/endpointSet.ts +++ b/packages/emulator/core/src/facility/endpointSet.ts @@ -41,16 +41,6 @@ import BotEndpoint from './botEndpoint'; const { decode } = base64Url; -function mapMap(map: { [key: string]: T }, mapper: (arg: T, val: string) => U): { [key: string]: U } { - return Object.keys(map).reduce( - (nextMap, key) => ({ - ...nextMap, - [key]: mapper.call(map, map[key], key), - }), - {} - ); -} - export default class Endpoints { private _endpoints: { [key: string]: BotEndpoint } = {}; constructor(private _options: BotEmulatorOptions) {} @@ -107,16 +97,6 @@ export default class Endpoints { } public getAll(): { [key: string]: BotEndpoint } { - return mapMap( - this._endpoints, - value => - ({ - botId: value.botId, - botUrl: value.botUrl, - msaAppId: value.msaAppId, - msaPassword: value.msaPassword, - use10Tokens: value.use10Tokens, - } as any) - ); + return this._endpoints; } } diff --git a/packages/emulator/core/src/facility/loggerAdapter.spec.ts b/packages/emulator/core/src/facility/loggerAdapter.spec.ts new file mode 100644 index 000000000..78b44a592 --- /dev/null +++ b/packages/emulator/core/src/facility/loggerAdapter.spec.ts @@ -0,0 +1,97 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { exceptionItem, inspectableObjectItem, LogLevel, summaryTextItem, textItem } from '@bfemulator/sdk-shared'; + +import LoggerAdapter from './loggerAdapter'; + +describe('LoggerAdapter', () => { + const mockLogToChat = jest.fn(() => null); + const logService: any = { + logToChat: mockLogToChat, + }; + let loggerAdapter: LoggerAdapter; + + beforeEach(() => { + loggerAdapter = new LoggerAdapter(logService); + mockLogToChat.mockClear(); + }); + + it('should log an activity to the user', () => { + const conversationId = 'convo1'; + const activity: any = { + type: 'text', + text: 'hello!', + }; + loggerAdapter.logActivity(conversationId, activity, 'user'); + + expect(mockLogToChat).toHaveBeenCalledWith( + conversationId, + textItem(LogLevel.Debug, '<-'), + inspectableObjectItem(activity.type, activity), + summaryTextItem(activity) + ); + }); + + it('should log an activity to the bot', () => { + const conversationId = 'convo1'; + const activity: any = { + type: 'text', + text: 'hi bot!', + }; + loggerAdapter.logActivity(conversationId, activity, 'bot'); + + expect(mockLogToChat).toHaveBeenCalledWith( + conversationId, + textItem(LogLevel.Debug, '->'), + inspectableObjectItem(activity.type, activity), + summaryTextItem(activity) + ); + }); + + it('should log a message', () => { + const conversationId = 'convo1'; + const messages: any = [{ type: 'text', payload: { text: 'hey' } }]; + loggerAdapter.logMessage(conversationId, ...messages); + + expect(mockLogToChat).toHaveBeenCalledWith(conversationId, ...messages); + }); + + it('should log an exception', () => { + const conversationId = 'convo1'; + const error = new Error('Something went horribly wrong! ;('); + loggerAdapter.logException(conversationId, error); + + expect(mockLogToChat).toHaveBeenCalledWith(conversationId, exceptionItem(error)); + }); +}); diff --git a/packages/emulator/core/src/facility/users.spec.ts b/packages/emulator/core/src/facility/users.spec.ts new file mode 100644 index 000000000..1415014b9 --- /dev/null +++ b/packages/emulator/core/src/facility/users.spec.ts @@ -0,0 +1,49 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Users from './users'; + +describe('Users', () => { + it('should initialize with a default current user id', () => { + const users = new Users(); + expect(users.currentUserId).toBe('default-user'); + }); + + it('should return a user by id', () => { + const user: any = { data: 'I am a user!' }; + const users = new Users(); + users.users = { id1: user }; + const retrievedUser = users.usersById('id1'); + expect(retrievedUser).toBe(user); + }); +}); diff --git a/packages/emulator/core/src/middleware/getFacility.spec.ts b/packages/emulator/core/src/middleware/getFacility.spec.ts new file mode 100644 index 000000000..bce1e428b --- /dev/null +++ b/packages/emulator/core/src/middleware/getFacility.spec.ts @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from './getFacility'; + +describe('getFacility', () => { + it('should get the facility', () => { + const facility = 'someFacility'; + const req: any = {}; + const res: any = {}; + const next: any = jest.fn(() => null); + const middleware = getFacility(facility); + middleware(req, res, next); + + expect(req.facility).toEqual(facility); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/middleware/getRouteName.spec.ts b/packages/emulator/core/src/middleware/getRouteName.spec.ts new file mode 100644 index 000000000..d2deec26f --- /dev/null +++ b/packages/emulator/core/src/middleware/getRouteName.spec.ts @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getRouteName from './getRouteName'; + +describe('getRouteName', () => { + it('should get the route name', () => { + const routeName = 'someRouteName'; + const req: any = {}; + const res: any = {}; + const next: any = jest.fn(() => null); + const middleware = getRouteName(routeName); + middleware(req, res, next); + + expect(req.routeName).toEqual(routeName); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/session/registerRoutes.spec.ts b/packages/emulator/core/src/session/registerRoutes.spec.ts new file mode 100644 index 000000000..4c5387041 --- /dev/null +++ b/packages/emulator/core/src/session/registerRoutes.spec.ts @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; + +import getSessionId from './middleware/getSessionId'; +import registerRoutes from './registerRoutes'; + +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('./middleware/getSessionId', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const server: any = { + get, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const facility = getFacility('directline'); + registerRoutes(emulator, server, uses); + + expect(get).toHaveBeenCalledWith( + '/v3/directline/session/getsessionid', + facility, + getRouteName('getSessionId'), + getSessionId(emulator) + ); + expect(get).toHaveBeenCalledWith('v4/token', jasmine.any(Function)); + }); +}); diff --git a/packages/emulator/core/src/userToken/registerRoutes.spec.ts b/packages/emulator/core/src/userToken/registerRoutes.spec.ts new file mode 100644 index 000000000..3bfed9d2b --- /dev/null +++ b/packages/emulator/core/src/userToken/registerRoutes.spec.ts @@ -0,0 +1,111 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import getBotEndpoint from '../middleware/getBotEndpoint'; +import getFacility from '../middleware/getFacility'; +import getRouteName from '../middleware/getRouteName'; +import createBotFrameworkAuthenticationMiddleware from '../utils/botFrameworkAuthentication'; +import createJsonBodyParserMiddleware from '../utils/jsonBodyParser'; + +import emulateOAuthCards from './middleware/emulateOAuthCards'; +import getToken from './middleware/getToken'; +import signOut from './middleware/signOut'; +import tokenResponse from './middleware/tokenResponse'; +import registerRoutes from './registerRoutes'; + +jest.mock('../middleware/getBotEndpoint', () => jest.fn(() => null)); +jest.mock('../middleware/getFacility', () => jest.fn(() => null)); +jest.mock('../middleware/getRouteName', () => jest.fn(() => null)); +jest.mock('../utils/botFrameworkAuthentication', () => jest.fn(() => null)); +jest.mock('../utils/jsonBodyParser', () => jest.fn(() => null)); +jest.mock('./middleware/emulateOAuthCards', () => jest.fn(() => null)); +jest.mock('./middleware/getToken', () => jest.fn(() => null)); +jest.mock('./middleware/signOut', () => jest.fn(() => null)); +jest.mock('./middleware/tokenResponse', () => jest.fn(() => null)); + +describe('registerRoutes', () => { + it('should register routes', () => { + const get = jest.fn(() => null); + const post = jest.fn(() => null); + const del = jest.fn(() => null); + const server: any = { + get, + post, + del, + }; + const uses = []; + const emulator: any = { + options: { fetch: () => null }, + }; + const jsonBodyParser = createJsonBodyParserMiddleware(); + const verifyBotFramework = createBotFrameworkAuthenticationMiddleware(emulator.options.fetch); + const botEndpoint = getBotEndpoint(emulator); + const facility = getFacility('api'); + registerRoutes(emulator, server, uses); + + expect(get).toHaveBeenCalledWith( + '/api/usertoken/GetToken', + verifyBotFramework, + botEndpoint, + facility, + getRouteName('getToken'), + getToken(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/api/usertoken/emulateOAuthCards', + verifyBotFramework, + facility, + getRouteName('emulateOAuthCards'), + emulateOAuthCards(emulator) + ); + + expect(del).toHaveBeenCalledWith( + '/api/usertoken/SignOut', + verifyBotFramework, + botEndpoint, + facility, + getRouteName('signOut'), + signOut(emulator) + ); + + expect(post).toHaveBeenCalledWith( + '/api/usertoken/tokenResponse', + ...uses, + jsonBodyParser, + facility, + getRouteName('tokenResponse'), + tokenResponse(emulator) + ); + }); +}); diff --git a/packages/emulator/core/src/userToken/tokenCache.spec.ts b/packages/emulator/core/src/userToken/tokenCache.spec.ts new file mode 100644 index 000000000..08183c039 --- /dev/null +++ b/packages/emulator/core/src/userToken/tokenCache.spec.ts @@ -0,0 +1,75 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { TokenCache } from './tokenCache'; + +describe('TokenCache tests', () => { + const botId = 'someBotId'; + const userId = 'someUserId'; + const connectionName = 'someConnectionName'; + const token = 'someToken'; + const tokenKey = `${botId}_${userId}_${connectionName}`; + + beforeEach(() => { + (TokenCache as any).tokenStore = {}; + }); + + it('should add a token to the cache', () => { + TokenCache.addTokenToCache(botId, userId, connectionName, token); + + expect((TokenCache as any).tokenStore[tokenKey]).toEqual({ + connectionName, + token, + }); + }); + + it('should delete a token from the cache', () => { + const tokenEntry = { connectionName, token }; + (TokenCache as any).tokenStore[tokenKey] = tokenEntry; + + TokenCache.deleteTokenFromCache(botId, userId, connectionName); + + expect((TokenCache as any).tokenStore[tokenKey]).toBe(undefined); + }); + + it('should get a token from the cache', () => { + const tokenEntry = { connectionName, token }; + (TokenCache as any).tokenStore[tokenKey] = tokenEntry; + + expect(TokenCache.getTokenFromCache(botId, userId, connectionName)).toBe(tokenEntry); + }); + + it('should generate a token key', () => { + expect((TokenCache as any).tokenKey(botId, userId, connectionName)).toBe(tokenKey); + }); +}); diff --git a/packages/emulator/core/src/utils/botFrameworkAuthentication.spec.ts b/packages/emulator/core/src/utils/botFrameworkAuthentication.spec.ts new file mode 100644 index 000000000..68afe34fa --- /dev/null +++ b/packages/emulator/core/src/utils/botFrameworkAuthentication.spec.ts @@ -0,0 +1,353 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { usGovernmentAuthentication, authentication, v32Authentication, v31Authentication } from '../authEndpoints'; + +import createBotFrameworkAuthenticationMiddleware from './botFrameworkAuthentication'; + +const mockGetKey = jest.fn().mockResolvedValue(`openIdMetadataKey`); +jest.mock('./openIdMetadata', () => { + return jest.fn().mockImplementation(() => ({ + getKey: mockGetKey, + })); +}); + +let mockDecode; +let mockVerify; +jest.mock('jsonwebtoken', () => ({ + get decode() { + return mockDecode; + }, + get verify() { + return mockVerify; + }, +})); + +describe('botFrameworkAuthenticationMiddleware', () => { + const authMiddleware = createBotFrameworkAuthenticationMiddleware(jest.fn().mockResolvedValue(true)); + const mockNext: any = jest.fn(() => null); + const mockStatus = jest.fn(() => null); + const mockEnd = jest.fn(() => null); + let mockPayload; + + beforeEach(() => { + mockNext.mockClear(); + mockEnd.mockClear(); + mockStatus.mockClear(); + mockDecode = jest.fn(() => ({ + header: { + kid: 'someKeyId', + }, + payload: mockPayload, + })); + mockVerify = jest.fn(() => 'verifiedJwt'); + mockGetKey.mockClear(); + }); + + it('should call the next middleware and return if there is no auth header', async () => { + const mockHeader = jest.fn(() => false); + const req: any = { header: mockHeader }; + const result = await authMiddleware(req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockHeader).toHaveBeenCalled(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should return a 401 if the token is not provided in the header', async () => { + mockDecode = jest.fn(() => null); + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockHeader).toHaveBeenCalled(); + expect(mockDecode).toHaveBeenCalledWith('someToken', { complete: true }); + expect(mockStatus).toHaveBeenCalledWith(401); + expect(mockEnd).toHaveBeenCalled(); + }); + + it('should return a 401 if a government bot provides a token in an unknown format', async () => { + mockPayload = { + aud: usGovernmentAuthentication.botTokenAudience, + ver: '99.9', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockStatus).toHaveBeenCalledWith(401); + expect(mockEnd).toHaveBeenCalled(); + }); + + it('should authenticate with a v1.0 gov token', async () => { + mockPayload = { + aud: usGovernmentAuthentication.botTokenAudience, + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: usGovernmentAuthentication.botTokenAudience, + clockTolerance: 300, + issuer: usGovernmentAuthentication.tokenIssuerV1, + }); + expect(req.jwt).toBe('verifiedJwt'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should authenticate with a v2.0 gov token', async () => { + mockPayload = { + aud: usGovernmentAuthentication.botTokenAudience, + ver: '2.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: usGovernmentAuthentication.botTokenAudience, + clockTolerance: 300, + issuer: usGovernmentAuthentication.tokenIssuerV2, + }); + expect(req.jwt).toBe('verifiedJwt'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should return a 401 if verifying a gov jwt token fails', async () => { + mockPayload = { + aud: usGovernmentAuthentication.botTokenAudience, + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + mockVerify = jest.fn(() => { + throw new Error('unverifiedJwt'); + }); + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: usGovernmentAuthentication.botTokenAudience, + clockTolerance: 300, + issuer: usGovernmentAuthentication.tokenIssuerV1, + }); + expect(req.jwt).toBeUndefined(); + expect(mockNext).not.toHaveBeenCalled(); + expect(mockStatus).toHaveBeenCalledWith(401); + expect(mockEnd).toHaveBeenCalled(); + }); + + it(`should return a 500 if a bot's token can't be retrieved from openId metadata`, async () => { + mockPayload = { + aud: 'not gov', + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + // key should come back as falsy + mockGetKey.mockResolvedValueOnce(null); + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockStatus).toHaveBeenCalledWith(500); + expect(mockEnd).toHaveBeenCalled(); + }); + + it('should return a 401 if a bot provides a token in an unknown format', async () => { + mockPayload = { + aud: 'not gov', + ver: '99.9', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockStatus).toHaveBeenCalledWith(401); + expect(mockEnd).toHaveBeenCalled(); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should authenticate with a v1.0 token', async () => { + mockPayload = { + aud: 'not gov', + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v32Authentication.tokenIssuerV1, + }); + expect(req.jwt).toBe('verifiedJwt'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should authenticate with a v2.0 token', async () => { + mockPayload = { + aud: 'not gov', + ver: '2.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v32Authentication.tokenIssuerV2, + }); + expect(req.jwt).toBe('verifiedJwt'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should attempt authentication with v3.1 characteristics if v3.2 auth fails', async () => { + mockPayload = { + aud: 'not gov', + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + // verification attempt with v3.2 token characteristics should fail + mockVerify.mockImplementationOnce(() => { + throw new Error('unverifiedJwt'); + }); + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledTimes(2); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v32Authentication.tokenIssuerV1, + }); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v31Authentication.tokenIssuer, + }); + expect(req.jwt).toBe('verifiedJwt'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should return a 401 if auth with both v3.1 & v3.2 token characteristics fail', async () => { + mockPayload = { + aud: 'not gov', + ver: '1.0', + }; + const mockHeader = jest.fn(() => 'Bearer someToken'); + const req: any = { header: mockHeader }; + const res: any = { + status: mockStatus, + end: mockEnd, + }; + mockVerify + // verification attempt with v3.2 token characteristics should fail + .mockImplementationOnce(() => { + throw new Error('unverifiedJwt'); + }) + // second attempt with v3.1 token characteristics should also fail + .mockImplementationOnce(() => { + throw new Error('unverifiedJwt'); + }); + const result = await authMiddleware(req, res, mockNext); + + expect(result).toBeUndefined(); + expect(mockVerify).toHaveBeenCalledTimes(2); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v32Authentication.tokenIssuerV1, + }); + expect(mockVerify).toHaveBeenCalledWith('someToken', 'openIdMetadataKey', { + audience: authentication.botTokenAudience, + clockTolerance: 300, + issuer: v31Authentication.tokenIssuer, + }); + expect(mockStatus).toHaveBeenCalledWith(401); + expect(mockEnd).toHaveBeenCalled(); + expect(req.jwt).toBeUndefined(); + expect(mockNext).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/utils/createResponse/apiException.spec.ts b/packages/emulator/core/src/utils/createResponse/apiException.spec.ts new file mode 100644 index 000000000..f1c15e44e --- /dev/null +++ b/packages/emulator/core/src/utils/createResponse/apiException.spec.ts @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import createApiException from './apiException'; + +describe('createApiException', () => { + it('should create an api exception', () => { + const exception = createApiException(500, '500', 'Internal server error.'); + + expect(exception.statusCode).toBe(500); + expect(exception.error).toEqual({ + error: { + code: '500', + message: 'Internal server error.', + }, + }); + }); +}); diff --git a/packages/emulator/core/src/utils/createResponse/conversation.spec.ts b/packages/emulator/core/src/utils/createResponse/conversation.spec.ts new file mode 100644 index 000000000..ba4d222bc --- /dev/null +++ b/packages/emulator/core/src/utils/createResponse/conversation.spec.ts @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import createConversationResponse from './conversation'; + +describe('createConversationResponse', () => { + it('should create a conversation response', () => { + const response = createConversationResponse('someId', 'someActivityId'); + + expect(response).toEqual({ + activityId: 'someActivityId', + id: 'someId', + }); + }); +}); diff --git a/packages/emulator/core/src/utils/createResponse/error.spec.ts b/packages/emulator/core/src/utils/createResponse/error.spec.ts new file mode 100644 index 000000000..3b235a6ac --- /dev/null +++ b/packages/emulator/core/src/utils/createResponse/error.spec.ts @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import createErrorResponse from './error'; + +describe('createErrorResponse', () => { + it('should create an error response', () => { + const response = createErrorResponse('404', 'Not found! :('); + + expect(response).toEqual({ + error: { + code: '404', + message: 'Not found! :(', + }, + }); + }); +}); diff --git a/packages/emulator/core/src/utils/createResponse/resource.spec.ts b/packages/emulator/core/src/utils/createResponse/resource.spec.ts new file mode 100644 index 000000000..a7106a726 --- /dev/null +++ b/packages/emulator/core/src/utils/createResponse/resource.spec.ts @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import createResourceResponse from './resource'; + +describe('createResourceResponse', () => { + it('should create a resource response', () => { + const response = createResourceResponse('someId'); + + expect(response).toEqual({ id: 'someId' }); + }); +}); diff --git a/packages/emulator/core/src/utils/jsonBodyParser.spec.ts b/packages/emulator/core/src/utils/jsonBodyParser.spec.ts new file mode 100644 index 000000000..fab9d6f10 --- /dev/null +++ b/packages/emulator/core/src/utils/jsonBodyParser.spec.ts @@ -0,0 +1,125 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import * as Restify from 'restify'; + +import jsonBodyParser from './jsonBodyParser'; + +let mockBodyParser = jest.fn(() => null); +jest.mock('restify', () => ({ + plugins: { + bodyReader: jest.fn(options => options), + jsonBodyParser: jest.fn(() => [mockBodyParser]), + }, +})); + +describe('jsonBodyParser', () => { + let bodyParsingFunctions; + const mockOptions: any = {}; + const mockNext: any = jest.fn(() => null); + + beforeEach(() => { + (Restify.plugins.bodyReader as any).mockClear(); + (Restify.plugins.jsonBodyParser as any).mockClear(); + bodyParsingFunctions = jsonBodyParser(mockOptions); + mockNext.mockClear(); + mockBodyParser.mockClear(); + }); + + it('should return a read function that reads a request body', () => { + // use default options + bodyParsingFunctions = jsonBodyParser(null); + + expect(bodyParsingFunctions.length).toBe(2); + expect(bodyParsingFunctions[0]).toEqual({ bodyReader: true, mapParams: false }); + + // use supplied options + bodyParsingFunctions = jsonBodyParser(mockOptions); + + expect(bodyParsingFunctions[0]).toEqual({ bodyReader: true }); + }); + + it('should not parse anything from a HEAD request', () => { + const req: any = { method: 'HEAD' }; + const result = jsonBodyParser(mockOptions)[1](req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should not parse anything from a GET request with no requestBodyOnGet option', () => { + mockOptions.requestBodyOnGet = false; + const req: any = { method: 'GET' }; + const result = jsonBodyParser(mockOptions)[1](req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should not parse anything from a non-chunked request with 0 content length', () => { + const req: any = { + method: 'POST', + contentLength: jest.fn(() => 0), + isChunked: jest.fn(() => false), + }; + const result = jsonBodyParser(mockOptions)[1](req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should parse json from the request content', () => { + const req: any = { + method: 'POST', + contentLength: jest.fn(() => 1), + contentType: jest.fn(() => 'application/json'), + }; + const result = jsonBodyParser(mockOptions)[1](req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockBodyParser).toHaveBeenCalled(); + }); + + it('should skip to the next middleware if there is no parser for the request body', () => { + const req: any = { + method: 'POST', + contentLength: jest.fn(() => 1), + contentType: jest.fn(() => 'application/json'), + }; + mockBodyParser = null; + const result = jsonBodyParser(mockOptions)[1](req, null, mockNext); + + expect(result).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/utils/oauthClientEncoder.spec.ts b/packages/emulator/core/src/utils/oauthClientEncoder.spec.ts new file mode 100644 index 000000000..ac75faa00 --- /dev/null +++ b/packages/emulator/core/src/utils/oauthClientEncoder.spec.ts @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import OAuthClientEncoder from './oauthClientEncoder'; + +describe('OAuthClientEncoder', () => { + it('should initialize with a conversation id', () => { + const activity: any = { conversation: { id: 'someId' } }; + const encoder = new OAuthClientEncoder(activity); + + expect((encoder as any)._conversationId).toBe(activity.conversation.id); + }); + + it('should initialize without a conversation id', () => { + const activity: any = { conversation: {} }; + const encoder = new OAuthClientEncoder(activity); + + expect((encoder as any)._conversationId).toBe(undefined); + }); + + it('should visit a card action', () => { + const encoder = new OAuthClientEncoder(null); + + expect((encoder as any).visitCardAction(null)).toBe(null); + }); + + it('should visit an oauth card action', () => { + const activity: any = { conversation: { id: 'someId' } }; + const connectionName = 'someConnectionName'; + const cardAction: any = { type: 'signin' }; + const encodedOAuthUrl = + OAuthClientEncoder.OAuthEmulatorUrlProtocol + '//' + connectionName + '&&&' + activity.conversation.id; + const encoder = new OAuthClientEncoder(activity); + (encoder as any).visitOAuthCardAction(connectionName, cardAction); + + expect(cardAction.type).toBe('openUrl'); + expect(cardAction.value).toEqual(encodedOAuthUrl); + }); +}); diff --git a/packages/emulator/core/src/utils/oauthLinkEncoder.spec.ts b/packages/emulator/core/src/utils/oauthLinkEncoder.spec.ts index 1a17da939..500a25963 100644 --- a/packages/emulator/core/src/utils/oauthLinkEncoder.spec.ts +++ b/packages/emulator/core/src/utils/oauthLinkEncoder.spec.ts @@ -1,12 +1,50 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + import { AttachmentContentTypes } from '@bfemulator/sdk-shared'; + import OAuthLinkEncoder from './oauthLinkEncoder'; + jest.mock('./uniqueId', () => () => 'fgfdsggf5432534'); + const mockArgsSentToFetch = []; let ok = true; let statusText = ''; let shouldThrow = false; + describe('The OauthLinkEncoder', () => { let encoder: OAuthLinkEncoder; + beforeAll(() => { (global as any).fetch = async (...args) => { mockArgsSentToFetch.push(args); diff --git a/packages/emulator/core/src/utils/openIdMetadata.spec.ts b/packages/emulator/core/src/utils/openIdMetadata.spec.ts new file mode 100644 index 000000000..6576bcb09 --- /dev/null +++ b/packages/emulator/core/src/utils/openIdMetadata.spec.ts @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import OpenIdMetadata from './openIdMetadata'; + +jest.mock('base64url', () => ({ + toBase64: jest.fn(value => `${value}_base64`), +})); +jest.mock('rsa-pem-from-mod-exp', () => jest.fn((n, e) => `${n}_${e}_pem`)); + +describe('OpenIdMetadata', () => { + it('should get a key and refresh cache if last updated more than 5 days ago', async () => { + const mockRefreshCache = jest.fn(() => Promise.resolve()); + const mockFindKey = jest.fn(() => 'someKey'); + const openIdMetadata = new OpenIdMetadata(null, null); + // ensure that the last time the keys were updated was more than 5 days ago + const lastUpdated = new Date().getTime() - 1000 * 60 * 60 * 24 * 6; + (openIdMetadata as any).lastUpdated = lastUpdated; + (openIdMetadata as any).refreshCache = mockRefreshCache; + (openIdMetadata as any).findKey = mockFindKey; + const key = await openIdMetadata.getKey('someKeyId'); + + expect(mockRefreshCache).toHaveBeenCalled(); + expect(mockFindKey).toHaveBeenCalledWith('someKeyId'); + expect(key).toBe('someKey'); + }); + + it('should refresh the cache', async () => { + /* eslint-disable typescript/camelcase */ + const mockFetch = jest + .fn() + // getting the openId config (resp1) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ jwks_uri: 'someJwksUri' }), + }) + // getting the keys (resp2) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ keys: ['key1', 'key2', 'key3'] }), + }); + const timeBeforeRefresh = new Date().getTime() - 1000; + const openIdMetadata = new OpenIdMetadata(mockFetch, 'someUrl'); + await (openIdMetadata as any).refreshCache(); + + expect(mockFetch).toHaveBeenCalledWith('someUrl'); + expect(mockFetch).toHaveBeenCalledWith('someJwksUri'); + expect((openIdMetadata as any).lastUpdated).toBeGreaterThan(timeBeforeRefresh); + expect((openIdMetadata as any).keys).toEqual(['key1', 'key2', 'key3']); + }); + + it('should throw when failing to get the openId config during a cache refresh', async () => { + // getting the openId config (resp1) + const mockFetch = jest.fn().mockResolvedValueOnce({ status: 401, statusCode: 401 }); + const openIdMetadata = new OpenIdMetadata(mockFetch, 'someUrl'); + + await expect((openIdMetadata as any).refreshCache()).rejects.toThrowError('Failed to load openID config: 401'); + }); + + it('should throw when failing to get the keys during a cache refresh', async () => { + /* eslint-disable typescript/camelcase */ + const mockFetch = jest + .fn() + // getting the openId config (resp1) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ jwks_uri: 'someJwksUri' }), + }) + // getting the keys (resp2) + .mockResolvedValueOnce({ + status: 404, + statusCode: 404, + }); + const openIdMetadata = new OpenIdMetadata(mockFetch, 'someUrl'); + + await expect((openIdMetadata as any).refreshCache()).rejects.toThrowError('Failed to load Keys: 404'); + }); + + it('should find a key', () => { + const keyId = 'someKeyId'; + const openIdMetadata = new OpenIdMetadata(null, null); + const key = { + kid: keyId, + n: 'someN', + e: 'someE', + }; + (openIdMetadata as any).keys = [key]; + const retrievedKey = (openIdMetadata as any).findKey(keyId); + expect(retrievedKey).toBe('someN_base64_someE_pem'); + }); + + it('should return null when trying to find keys if the keys array is undefined', () => { + const openIdMetadata = new OpenIdMetadata(null, null); + (openIdMetadata as any).keys = undefined; + expect((openIdMetadata as any).findKey('someKeyId')).toBe(null); + }); + + it('should return null when trying to find a non-RSA key', () => { + const keyId1 = 'someKeyId1'; + const keyId2 = 'someKeyId2'; + const openIdMetadata = new OpenIdMetadata(null, null); + const key1 = { + kid: keyId1, + n: 'someN', + }; + const key2 = { + kid: keyId2, + e: 'someE', + }; + (openIdMetadata as any).keys = [key1, key2]; + // no e + const retrievedKey1 = (openIdMetadata as any).findKey(keyId1); + + expect(retrievedKey1).toBe(null); + + // no n + const retrievedKey2 = (openIdMetadata as any).findKey(keyId2); + + expect(retrievedKey2).toBe(null); + }); + + it('should return null if it cannot find the specified key', () => { + const openIdMetadata = new OpenIdMetadata(null, null); + (openIdMetadata as any).keys = []; + expect((openIdMetadata as any).findKey('someKeyId')).toBe(null); + }); +}); diff --git a/packages/emulator/core/src/utils/paymentEncoder.spec.ts b/packages/emulator/core/src/utils/paymentEncoder.spec.ts new file mode 100644 index 000000000..13a8f4f69 --- /dev/null +++ b/packages/emulator/core/src/utils/paymentEncoder.spec.ts @@ -0,0 +1,62 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import PaymentEncoder from './paymentEncoder'; + +describe('PaymentEncoder', () => { + it('should encode a payment card action', () => { + const paymentEncoder = new PaymentEncoder(); + const cardAction: any = { + type: 'payment', + value: { + id: 'someId', + onshippingaddresschange: null, + onshippingoptionchange: null, + shippingAddress: null, + shippingOption: null, + shippingType: null, + abort: null, + canMakePayment: null, + show: null, + addEventListener: null, + removeEventListener: null, + }, + }; + const paymentRequest = cardAction.value as PaymentRequest; + const encodedPaymentUrl = PaymentEncoder.PaymentEmulatorUrlProtocol + '//' + JSON.stringify(paymentRequest); + (paymentEncoder as any).visitCardAction(cardAction); + + expect(cardAction.type).toBe('openUrl'); + expect(cardAction.value).toEqual(encodedPaymentUrl); + }); +}); diff --git a/packages/emulator/core/src/utils/sendErrorResponse.spec.ts b/packages/emulator/core/src/utils/sendErrorResponse.spec.ts new file mode 100644 index 000000000..82196a6bc --- /dev/null +++ b/packages/emulator/core/src/utils/sendErrorResponse.spec.ts @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { ErrorCodes } from '@bfemulator/sdk-shared'; +import * as HttpStatus from 'http-status-codes'; + +import sendErrorResponse from './sendErrorResponse'; +import createErrorResponse from './createResponse/error'; + +describe('sendErrorResponse', () => { + const mockSend = jest.fn(() => {}); + const mockEnd = jest.fn(() => {}); + const response: any = { end: mockEnd, send: mockSend }; + + beforeEach(() => { + mockSend.mockClear(); + mockEnd.mockClear(); + }); + + it('should send an error response with the provided exception', () => { + const exception = { error: 'some error', statusCode: 404 }; + const error = sendErrorResponse(null, response, null, exception); + + expect(error).toBe(exception.error); + expect(mockSend).toHaveBeenCalledWith(exception.statusCode, exception.error); + expect(mockEnd).toHaveBeenCalled(); + }); + + it('should create and send an error response when no status code / exception is provided', () => { + const exception = { message: 'some error message' }; + const error = sendErrorResponse(null, response, null, exception); + + expect(error).toEqual(createErrorResponse(ErrorCodes.ServiceError, exception.message)); + expect(mockSend).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST, error); + expect(mockEnd).toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/utils/statusCodeFamily.spec.ts b/packages/emulator/core/src/utils/statusCodeFamily.spec.ts new file mode 100644 index 000000000..6d118f789 --- /dev/null +++ b/packages/emulator/core/src/utils/statusCodeFamily.spec.ts @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import statusCodeFamily from './statusCodeFamily'; + +describe('statusCodeFamily', () => { + it('should return whether a status code is within the expected family', () => { + expect(statusCodeFamily(201, 200)).toBe(true); + + expect(statusCodeFamily('404', 500)).toBe(false); + }); +}); diff --git a/packages/emulator/core/src/utils/stripEmptyBearerToken.spec.ts b/packages/emulator/core/src/utils/stripEmptyBearerToken.spec.ts new file mode 100644 index 000000000..b6865fd88 --- /dev/null +++ b/packages/emulator/core/src/utils/stripEmptyBearerToken.spec.ts @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import stripEmptyBearerToken from './stripEmptyBearerToken'; + +describe('stripEmptyBearerToken', () => { + it('should create a middleware that strips the empty bearer token', () => { + const middleware = stripEmptyBearerToken(); + const next = jest.fn(() => null); + const request = { headers: { authorization: 'Bearer' } }; + middleware(request, null, next); + + expect(request.headers.authorization).toBe(undefined); + expect(next).toHaveBeenCalled(); + }); + + it('should not attempt to strip the auth header if it does not exist', () => { + const middleware = stripEmptyBearerToken(); + const next = jest.fn(() => null); + const request = { headers: { authorization: null } }; + middleware(request, null, next); + + expect(request.headers.authorization).toBe(null); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/packages/emulator/core/src/utils/uniqueId.spec.ts b/packages/emulator/core/src/utils/uniqueId.spec.ts new file mode 100644 index 000000000..6aaa40170 --- /dev/null +++ b/packages/emulator/core/src/utils/uniqueId.spec.ts @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import uniqueId from './uniqueId'; + +describe('uniqueId', () => { + it('should generate unique ids', () => { + const id1 = uniqueId(); + const id2 = uniqueId(); + const id3 = uniqueId(); + + expect(id1).not.toBe(id2); + expect(id1).not.toBe(id3); + expect(id2).not.toBe(id3); + }); +});