diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 795bc4eef5..6f7d5895a1 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -135,7 +135,9 @@ const USER_AGENT: string = `Microsoft-BotFramework/3.1 BotBuilder/${ pjson.versi `(Node.js,Version=${ NODE_VERSION }; ${ TYPE } ${ RELEASE }; ${ ARCHITECTURE })`; const OAUTH_ENDPOINT = 'https://api.botframework.com'; const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us'; -const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse'); + +// This key is exported internally so that the TeamsActivityHandler will not overwrite any already set InvokeResponses. +export const INVOKE_RESPONSE_KEY: symbol = Symbol('invokeResponse'); /** * A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint. diff --git a/libraries/botbuilder/src/teamsActivityHandler.ts b/libraries/botbuilder/src/teamsActivityHandler.ts index 176b8326c5..ce36ae554c 100644 --- a/libraries/botbuilder/src/teamsActivityHandler.ts +++ b/libraries/botbuilder/src/teamsActivityHandler.ts @@ -1,13 +1,16 @@ +/** + * @module botbuilder + */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { InvokeResponse } from './botFrameworkAdapter'; +import { InvokeResponse, INVOKE_RESPONSE_KEY } from './botFrameworkAdapter'; import { - ActivityTypes, ActivityHandler, + ActivityTypes, AppBasedLinkQuery, ChannelAccount, ChannelInfo, @@ -37,7 +40,8 @@ export class TeamsActivityHandler extends ActivityHandler { switch (context.activity.type) { case ActivityTypes.Invoke: const invokeResponse = await this.onInvokeActivity(context); - if (invokeResponse) { + // If onInvokeActivity has already sent an InvokeResponse, do not send another one. + if (invokeResponse && !context.turnState.get(INVOKE_RESPONSE_KEY)) { await context.sendActivity({ value: invokeResponse, type: 'invokeResponse' }); } break; @@ -58,7 +62,8 @@ export class TeamsActivityHandler extends ActivityHandler { } else { switch (context.activity.name) { case 'signin/verifyState': - return await this.onTeamsSigninVerifyState(context, context.activity.value); + await this.onTeamsSigninVerifyState(context, context.activity.value); + return TeamsActivityHandler.createInvokeResponse(); case 'fileConsent/invoke': return TeamsActivityHandler.createInvokeResponse(await this.onTeamsFileConsent(context, context.activity.value)); @@ -94,15 +99,10 @@ export class TeamsActivityHandler extends ActivityHandler { return TeamsActivityHandler.createInvokeResponse(); case 'task/fetch': - const fetchResponse = await this.onTeamsTaskModuleFetch(context, context.activity.value); - const taskModuleContineResponse = { type: 'continue', value: fetchResponse }; - const taskModuleResponse = { task: taskModuleContineResponse }; - return TeamsActivityHandler.createInvokeResponse(taskModuleResponse); + return TeamsActivityHandler.createInvokeResponse(await this.onTeamsTaskModuleFetch(context, context.activity.value)); case 'task/submit': - const submitResponseBase = await this.onTeamsTaskModuleSubmit(context, context.activity.value); - const taskModuleResponse_submit = { task: submitResponseBase }; - return TeamsActivityHandler.createInvokeResponse(taskModuleResponse_submit); + return TeamsActivityHandler.createInvokeResponse(await this.onTeamsTaskModuleSubmit(context, context.activity.value)); default: throw new Error('NotImplemented'); @@ -183,7 +183,7 @@ export class TeamsActivityHandler extends ActivityHandler { * @param context * @param action */ - protected async onTeamsSigninVerifyState(context: TurnContext, query: SigninStateVerificationQuery): Promise { + protected async onTeamsSigninVerifyState(context: TurnContext, query: SigninStateVerificationQuery): Promise { throw new Error('NotImplemented'); } @@ -201,7 +201,7 @@ export class TeamsActivityHandler extends ActivityHandler { * @param context * @param taskModuleRequest */ - protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { throw new Error('NotImplemented'); } @@ -210,7 +210,7 @@ export class TeamsActivityHandler extends ActivityHandler { * @param context * @param taskModuleRequest */ - protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { throw new Error('NotImplemented'); } diff --git a/libraries/botbuilder/src/teamsActivityHelpers.ts b/libraries/botbuilder/src/teamsActivityHelpers.ts index b7d35dcd4e..1d94aadc03 100644 --- a/libraries/botbuilder/src/teamsActivityHelpers.ts +++ b/libraries/botbuilder/src/teamsActivityHelpers.ts @@ -1,35 +1,51 @@ +/** + * @module botbuilder + */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ +import { + Activity, + ChannelInfo, + NotificationInfo, + TeamInfo, + TeamsChannelData +} from 'botbuilder-core'; /** * Activity helper methods for Teams. */ +export function teamsGetChannelId(activity: Activity): string { + if (!activity) { + throw new Error('Missing activity parameter'); + } -export function teamsGetChannelId(activity : object) { - const channelData = ('channelData' in activity) ? activity['channelData'] : null; - const channel = (validObject(channelData) && 'channel' in channelData) ? channelData['channel'] : null; - return (validObject(channel) && 'id' in channel) ? channel['id'] : null; + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + const channel: ChannelInfo = channelData ? channelData.channel : null; + return channel && channel.id ? channel.id : null; } -export function teamsGetTeamId(activity : object) { - const channelData = ('channelData' in activity) ? activity['channelData'] : null; - const team = (validObject(channelData) && 'team' in channelData) ? channelData['team'] : null; - return (validObject(team) && 'id' in team) ? team['id'] : null; +export function teamsGetTeamId(activity: Activity): string { + if (!activity) { + throw new Error('Missing activity parameter'); + } + + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + const team: TeamInfo = channelData ? channelData.team : null; + return team && team.id ? team.id : null; } -export function teamsNotifyUser(activity : object) { - const channelData = (validObject(activity) && 'channelData' in activity) ? activity['channelData'] : { }; - channelData['Notification'] = { Alert: true }; - activity['channelData'] = channelData; -} +export function teamsNotifyUser(activity: Activity): void { + if (!activity) { + throw new Error('Missing activity parameter'); + } -function validObject(activity) { - // Check make sure not a string - if (activity == null || activity == undefined || activity instanceof String || typeof(activity) == 'string' ) { - return false; + if (!activity.channelData || typeof activity.channelData !== 'object') { + activity.channelData = {}; } - return true; -} \ No newline at end of file + + const channelData: TeamsChannelData = activity.channelData as TeamsChannelData; + channelData.notification = { alert: true } as NotificationInfo; +} diff --git a/libraries/botbuilder/src/teamsInfo.ts b/libraries/botbuilder/src/teamsInfo.ts index 429d53f3ca..216c302a42 100644 --- a/libraries/botbuilder/src/teamsInfo.ts +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -1,3 +1,6 @@ +/** + * @module botbuilder + */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. @@ -21,6 +24,7 @@ export class TeamsInfo { if (!teamId) { throw new Error('This method is only valid within the scope of a MS Teams Team.'); } + return await this.getTeamsConnectorClient(context).teams.fetchTeamDetails(teamId); } @@ -29,6 +33,7 @@ export class TeamsInfo { if (!teamId) { throw new Error('This method is only valid within the scope of a MS Teams Team.'); } + const channelList: ConversationList = await this.getTeamsConnectorClient(context).teams.fetchChannelList(teamId); return channelList.conversations; } @@ -49,10 +54,12 @@ export class TeamsInfo { if (!conversationId) { throw new Error('The getMembers operation needs a valid conversationId.'); } + const teamMembers = await connectorClient.conversations.getConversationMembers(conversationId); teamMembers.forEach((member:any) => { member.aadObjectId = member.objectId; }); + return teamMembers as TeamsChannelAccount[]; } @@ -60,9 +67,11 @@ export class TeamsInfo { if (!context) { throw new Error('Missing context parameter'); } + if (!context.activity) { throw new Error('Missing activity on context'); } + const channelData = context.activity.channelData as TeamsChannelData; const team = channelData && channelData.team ? channelData.team : undefined; const teamId = team && typeof(team.id) === 'string' ? team.id : undefined; @@ -71,8 +80,9 @@ export class TeamsInfo { private static getConnectorClient(context: TurnContext): ConnectorClient { if (!context.adapter || !('createConnectorClient' in context.adapter)) { - throw new Error('This method requires a connector client.') + throw new Error('This method requires a connector client.'); } + return (context.adapter as BotFrameworkAdapter).createConnectorClient(context.activity.serviceUrl); } @@ -80,5 +90,4 @@ export class TeamsInfo { const connectorClient = this.getConnectorClient(context); return new TeamsConnectorClient(connectorClient.credentials, { baseUri: context.activity.serviceUrl }); } - -} \ No newline at end of file +} diff --git a/libraries/botbuilder/src/teamsTurnContextHelpers.ts b/libraries/botbuilder/src/teamsTurnContextHelpers.ts index f4157892bb..84c514351f 100644 --- a/libraries/botbuilder/src/teamsTurnContextHelpers.ts +++ b/libraries/botbuilder/src/teamsTurnContextHelpers.ts @@ -1,17 +1,21 @@ +/** + * @module botbuilder + */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ + import { Activity, ChannelInfo, ConversationParameters, ConversationReference, ConversationResourceResponse, - ResourceResponse, TeamsChannelData, - TurnContext, + TurnContext } from 'botbuilder-core'; + import { teamsGetTeamId } from './teamsActivityHelpers'; import { BotFrameworkAdapter } from './botFrameworkAdapter'; @@ -19,7 +23,7 @@ import { BotFrameworkAdapter } from './botFrameworkAdapter'; * Turn Context extension methods for Teams. */ -export async function teamsCreateConversation(turnContext: TurnContext, teamsChannelId: string, message: Partial): Promise<[ConversationReference, string]> { +export async function teamsCreateConversation(context: TurnContext, teamsChannelId: string, message: Partial): Promise<[ConversationReference, string]> { if (!teamsChannelId) { throw new Error('Missing valid teamsChannelId argument'); } @@ -33,14 +37,22 @@ export async function teamsCreateConversation(turnContext: TurnContext, teamsCha id: teamsChannelId } }, - activity: message, + activity: message, }; - const adapter = turnContext.adapter; - const connectorClient = adapter.createConnectorClient(turnContext.activity.serviceUrl); + const adapter = context.adapter; + const connectorClient = adapter.createConnectorClient(context.activity.serviceUrl); // This call does NOT send the outbound Activity is not being sent through the middleware stack. const conversationResourceResponse: ConversationResourceResponse = await connectorClient.conversations.createConversation(conversationParameters); - const conversationReference = TurnContext.getConversationReference(turnContext.activity); + const conversationReference = TurnContext.getConversationReference(context.activity); conversationReference.conversation.id = conversationResourceResponse.id; return [conversationReference, conversationResourceResponse.activityId]; } +export async function teamsSendToGeneralChannel(context: TurnContext, message: Partial): Promise<[ConversationReference, string]> { + const teamId = teamsGetTeamId(context.activity); + if (!teamId) { + throw new Error('The current Activity was not sent from a Teams Team.'); + } + + return teamsCreateConversation(context, teamId, message); +} diff --git a/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts b/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts index 3ea1fd5bea..1bba1a5e76 100644 --- a/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts +++ b/libraries/botbuilder/tests/teams/adaptiveCards/src/adaptiveCardsBot.ts @@ -5,12 +5,13 @@ import { CardFactory, InvokeResponse, MessageFactory, + TaskModuleContinueResponse, TaskModuleMessageResponse, TaskModuleRequest, - TaskModuleResponseBase, + TaskModuleResponse, TaskModuleTaskInfo, TeamsActivityHandler, - TurnContext + TurnContext, } from 'botbuilder'; /** @@ -66,7 +67,7 @@ export class AdaptiveCardsBot extends TeamsActivityHandler { }); } - protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { await context.sendActivity(MessageFactory.text(`OnTeamsTaskModuleFetchAsync TaskModuleRequest: ${JSON.stringify(taskModuleRequest)}`)); /** @@ -96,17 +97,28 @@ export class AdaptiveCardsBot extends TeamsActivityHandler { "version": "1.0" }); /* tslint:enable:quotemark object-literal-key-quotes */ - return { - card, - height: 200, - title: 'Task Module Example', - width: 400 - } as TaskModuleTaskInfo; + return { + task: { + type: 'continue', + value: { + card, + height: 200, + title: 'Task Module Example', + width: 400 + } as TaskModuleTaskInfo + } as TaskModuleContinueResponse + } as TaskModuleResponse; } - protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { await context.sendActivity(MessageFactory.text(`OnTeamsTaskModuleSubmit value: ${JSON.stringify(taskModuleRequest)}`)); - return { type: 'message', value: 'Thanks!' } as TaskModuleMessageResponse; + + return { + task: { + type: 'message', + value: 'Thanks!' + } as TaskModuleMessageResponse + } as TaskModuleResponse; } protected async onTeamsCardActionInvoke(context: TurnContext): Promise { diff --git a/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts index 5a6cd4949b..bf30bb154d 100644 --- a/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts +++ b/libraries/botbuilder/tests/teams/messagingExtensionAuth/src/messagingExtensionAuthBot.ts @@ -6,8 +6,8 @@ import { CardFactory, MessagingExtensionActionResponse, MessagingExtensionAction, - MessagingExtensionQuery, TaskModuleContinueResponse, + TaskModuleResponse, TaskModuleTaskInfo, TaskModuleRequest, TeamsActivityHandler, @@ -107,13 +107,23 @@ export class MessagingExtensionAuthBot extends TeamsActivityHandler { return response; } - protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { var data = context.activity.value; if (data && data.state) { const adapter: IUserTokenProvider = context.adapter as BotFrameworkAdapter; const tokenResponse = await adapter.getUserToken(context, this.connectionName, data.state); - return this.CreateSignedInTaskModuleTaskInfo(tokenResponse.token); + + const continueResponse : TaskModuleContinueResponse = { + type: 'continue', + value: this.CreateSignedInTaskModuleTaskInfo(tokenResponse.token), + }; + + const response : MessagingExtensionActionResponse = { + task: continueResponse + }; + + return response; } else { diff --git a/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts b/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts index b053cf712d..2212dcff8b 100644 --- a/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts +++ b/libraries/botbuilder/tests/teams/taskModule/src/taskModuleBot.ts @@ -8,9 +8,10 @@ import { Attachment, CardFactory, MessageFactory, + TaskModuleContinueResponse, TaskModuleMessageResponse, TaskModuleRequest, - TaskModuleResponseBase, + TaskModuleResponse, TaskModuleTaskInfo, TurnContext, } from 'botbuilder-core'; @@ -28,22 +29,33 @@ export class TaskModuleBot extends TeamsActivityHandler { }); } - protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + protected async onTeamsTaskModuleFetch(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { var reply = MessageFactory.text("OnTeamsTaskModuleFetchAsync TaskModuleRequest" + JSON.stringify(taskModuleRequest)); await context.sendActivity(reply); + return { - card: this.GetTaskModuleAdaptiveCard(), - height: 200, - width: 400, - title: "Adaptive Card: Inputs", - }; + task: { + type: "continue", + value: { + card: this.GetTaskModuleAdaptiveCard(), + height: 200, + width: 400, + title: "Adaptive Card: Inputs", + } as TaskModuleTaskInfo, + } as TaskModuleContinueResponse + } as TaskModuleResponse; } - // TaskModuleResponseBase: type: - protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { + + protected async onTeamsTaskModuleSubmit(context: TurnContext, taskModuleRequest: TaskModuleRequest): Promise { var reply = MessageFactory.text("OnTeamsTaskModuleFetchAsync Value: " + JSON.stringify(taskModuleRequest)); await context.sendActivity(reply); - var response : TaskModuleMessageResponse = { type: "message", value: "Hello", }; - return response ; + + return { + task: { + type: "message", + value: "Hello", + } as TaskModuleMessageResponse + } as TaskModuleResponse; } private GetTaskModuleHeroCard() : Attachment { diff --git a/libraries/botbuilder/tests/teamsActivityHandler.test.js b/libraries/botbuilder/tests/teamsActivityHandler.test.js index 1e246d6fbc..f1c07faa6a 100644 --- a/libraries/botbuilder/tests/teamsActivityHandler.test.js +++ b/libraries/botbuilder/tests/teamsActivityHandler.test.js @@ -13,22 +13,52 @@ function createInvokeActivity(name, value = {}, channelData = {}) { } describe('TeamsActivityHandler', () => { - it('should call onTurnActivity if non-Invoke is received', async () => { - const bot = new TeamsActivityHandler(); - bot.onMessage(async (context, next) => { - await context.sendActivity('Hello'); - await next(); - }); - - const adapter = new TestAdapter(async context => { - await bot.run(context); + describe('onTurnActivity()', () => { + it('should not override the InvokeResponse on the context.turnState if it is set', done => { + class InvokeHandler extends TeamsActivityHandler { + async onInvokeActivity(context) { + assert(context, 'context not found'); + await context.sendActivity({ type: 'invokeResponse', value: { status: 200, body: `I'm a teapot.` } }); + return { status: 418 }; + } + } + + const bot = new InvokeHandler(); + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send({ type: ActivityTypes.Invoke }) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, `I'm a teapot.`); + done(); + }) + .catch(err => done(err)); + }); - adapter.send({ type: ActivityTypes.Message, text: 'Hello' }) - .assertReply(activity => { - assert.strictEqual(activity.type, ActivityTypes.Message); - assert.strictEqual(activity.text, 'Hello'); + it('should call onTurnActivity if non-Invoke is received', done => { + const bot = new TeamsActivityHandler(); + bot.onMessage(async (context, next) => { + await context.sendActivity('Hello'); + await next(); }); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + adapter.send({ type: ActivityTypes.Message, text: 'Hello' }) + .assertReply(activity => { + assert.strictEqual(activity.type, ActivityTypes.Message); + assert.strictEqual(activity.text, 'Hello'); + done(); + }) + .catch(err => done(err)); + }); }); describe('should send a BadRequest status code if', () => { @@ -233,7 +263,7 @@ describe('TeamsActivityHandler', () => { }); }); - describe('should send an OK status code when', () => { + describe('should send an OK status code', () => { class OKFileConsent extends TeamsActivityHandler { async onTeamsFileConsentAccept(context, fileConsentCardResponse) { assert(context, 'context not found'); @@ -245,7 +275,8 @@ describe('TeamsActivityHandler', () => { assert(fileConsentCardResponse, 'fileConsentCardResponse not found'); } } - it('a "fileConsent/invoke" activity is handled by onTeamsFileConsentAccept', async () => { + + it('when a "fileConsent/invoke" activity is handled by onTeamsFileConsentAccept', async () => { const bot = new OKFileConsent(); const adapter = new TestAdapter(async context => { @@ -262,7 +293,7 @@ describe('TeamsActivityHandler', () => { }); }); - it('a "fileConsent/invoke" activity is handled by onTeamsFileConsentDecline', async () => { + it('when a "fileConsent/invoke" activity is handled by onTeamsFileConsentDecline', async () => { const bot = new OKFileConsent(); const adapter = new TestAdapter(async context => { @@ -279,7 +310,7 @@ describe('TeamsActivityHandler', () => { }); }); - it('a "fileConsent/invoke" activity handled by onTeamsFileConsent', async () => { + it('when a "fileConsent/invoke" activity handled by onTeamsFileConsent', async () => { class FileConsent extends TeamsActivityHandler { async onTeamsFileConsent(context, fileConsentCardResponse) { assert(context, 'context not found'); @@ -301,6 +332,68 @@ describe('TeamsActivityHandler', () => { `expected empty body for invokeResponse from fileConsent flow.\nReceived: ${JSON.stringify(activity.value.body)}`); }); }); + + + describe('and the return value from', () => { + class TaskHandler extends TeamsActivityHandler { + constructor() { + super(); + // TaskModuleResponses with inner types of 'continue' and 'message'. + this.fetchReturn = { task: { type: 'continue', value: { title: 'test' } } }; + this.submitReturn = { task: { type: 'message', value: 'test' } }; + } + + async onTeamsTaskModuleFetch(context, taskModuleRequest) { + assert(context, 'context not found'); + assert(taskModuleRequest, 'taskModuleRequest not found'); + return this.fetchReturn; + } + + async onTeamsTaskModuleSubmit(context, taskModuleRequest) { + assert(context, 'context not found'); + assert(taskModuleRequest, 'taskModuleRequest not found'); + return this.submitReturn; + } + } + + it('an overriden onTeamsTaskModuleFetch()', done => { + const bot = new TaskHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskFetchActivity = createInvokeActivity('task/fetch', { data: 'fetch' }); + adapter.send(taskFetchActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, bot.fetchReturn); + done(); + }) + .catch(err => done(err)); + }); + + it('an overriden onTeamsTaskModuleSubmit()', done => { + const bot = new TaskHandler(); + + const adapter = new TestAdapter(async context => { + await bot.run(context); + }); + + const taskSubmitActivity = createInvokeActivity('task/submit', { data: 'submit' }); + adapter.send(taskSubmitActivity) + .assertReply(activity => { + assert.strictEqual(activity.type, 'invokeResponse'); + assert(activity.value, 'activity.value not found'); + assert.strictEqual(activity.value.status, 200); + assert.strictEqual(activity.value.body, bot.submitReturn); + done(); + }) + .catch(err => done(err)); + }); + }); }); describe('should send a BadRequest status code when', () => { @@ -317,7 +410,6 @@ describe('TeamsActivityHandler', () => { assert.strictEqual(activity.value.status, 400); assert.strictEqual(activity.value.body, undefined); }); - }); it('onTeamsMessagingExtensionSubmitActionDispatch() receives an unexpected botMessagePreviewAction value', () => { diff --git a/libraries/botbuilder/tests/teamsHelpers.test.js b/libraries/botbuilder/tests/teamsHelpers.test.js index 3cdf806958..3a43d1bb99 100644 --- a/libraries/botbuilder/tests/teamsHelpers.test.js +++ b/libraries/botbuilder/tests/teamsHelpers.test.js @@ -4,15 +4,22 @@ */ const assert = require('assert'); -var sinon = require('sinon'); -const { teamsGetTeamId, teamsNotifyUser, teamsCreateConversation, BotFrameworkAdapter } = require('../'); -const { TestAdapter, TurnContext, Conversations } = require('botbuilder-core'); -const { ConnectorClient, } = require('botframework-connector'); +const { TurnContext } = require('botbuilder-core'); +const sinon = require('sinon'); + +const { + BotFrameworkAdapter, + teamsCreateConversation, + teamsGetChannelId, + teamsGetTeamId, + teamsNotifyUser, + teamsSendToGeneralChannel +} = require('../'); class TestContext extends TurnContext { constructor(request) { - var adapter = new BotFrameworkAdapter(); + const adapter = new BotFrameworkAdapter(); sinon.stub(adapter, 'createConnectorClient') .withArgs('http://foo.com/api/messages') .returns({ conversations: { @@ -35,73 +42,178 @@ class TestContext extends TurnContext { } } -describe('ActivityExtensions', function() { +describe('TeamsActivityHelpers method', function() { + describe('teamsGetChannelId()', () => { + it('should return null if activity.channelData is falsey', () => { + const channelId = teamsGetChannelId(createActivityNoChannelData()); + assert(channelId === null); + }); - it('should get team id', async function() { - const activity = createActivityTeamId(); - const id = teamsGetTeamId(activity); - assert(id === 'myId'); - }); - it('should get null with no team id', async function() { - const activity = createActivityNoId(); - const id = teamsGetTeamId(activity); - assert(id === null); - }); - it('should get null with no channelData', async function() { - const activity = createActivityNoChannelData(); - const id = teamsGetTeamId(activity); - assert(id === null); - }); - it('should add notify with no notification in channelData', async function() { - var activity = createActivityTeamId(); - teamsNotifyUser(activity); - assert(activity.channelData.Notification.Alert === true); - }); - it('should add notify with no channelData', async function() { - var activity = createActivityNoChannelData(); - teamsNotifyUser(activity); - assert(activity.channelData.Notification.Alert === true); - }); - it('should error with no teamsChannelId', async function() { - // Arrange - const context = new TestContext(createActivityNoId()); - // Act - await teamsCreateConversation(context, null, createActivityNoId()).catch((error) => { - // Assert - assert(error.message == 'Missing valid teamsChannelId argument'); + it('should return null if activity.channelData.channel is falsey', () => { + const activity = createActivityTeamId(); + const channelId = teamsGetChannelId(activity); + assert(channelId === null); + }); + + it('should return null if activity.channelData.channel.id is falsey', () => { + const activity = createActivityTeamId(); + activity.channelData.channel = {}; + const channelId = teamsGetChannelId(activity); + assert(channelId === null); + }); + + it('should return channel id', () => { + const activity = createActivityTeamId(); + activity.channelData.channel = { id: 'channelId' }; + const channelId = teamsGetChannelId(activity); + assert.strictEqual(channelId, 'channelId'); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsGetChannelId(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } }); }); - it('should error with no activity', async function() { - // Arrange - const context = new TestContext(createActivityNoId()); - // Act - await teamsCreateConversation(context, "msteams", null).catch((error) => { - // Assert - assert(error.message == 'Missing valid message argument'); + + describe('teamsGetTeamId()', () => { + it('should return team id', async function() { + const activity = createActivityTeamId(); + const id = teamsGetTeamId(activity); + assert(id === 'myId'); + }); + + it('should return null with no team id', async function() { + const activity = createActivityNoTeamId(); + const id = teamsGetTeamId(activity); + assert(id === null); + }); + + it('should return null with no channelData', async function() { + const activity = createActivityNoChannelData(); + const id = teamsGetTeamId(activity); + assert(id === null); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsGetTeamId(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } }); }); - it('should get results from teamsCreateConversation', async function() { - // Arrange - const context = new TestContext(createActivityNoId()); - - // Act - const result = await teamsCreateConversation(context, "mycrazyteamschannel", createActivityNoId()); - - // Assert - assert(result); - assert(result.length == 2); - assert(result[0]); - assert(result[1]); - assert(result[1] === "MYACTIVITYID"); - assert(result[0].activityId == 1); - assert(result[0].conversation.id == 'MyCreationId'); - assert(result[0].channelId == 'teams'); + + describe('teamsNotifyUser()', () => { + it('should add notify with no notification in channelData', async function() { + const activity = createActivityTeamId(); + teamsNotifyUser(activity); + assert(activity.channelData.notification.alert === true); + }); + + it('should add notify with no channelData', async function() { + const activity = createActivityNoChannelData(); + teamsNotifyUser(activity); + assert(activity.channelData.notification.alert === true); + }); + + it('should throw an error if no activity is passed in', () => { + try { + teamsNotifyUser(undefined); + } catch (err) { + assert.strictEqual(err.message, 'Missing activity parameter'); + } + }); }); }); +describe('TeamsTurnContextHelpers method', () => { + describe('teamsCreateConversation()', () => { + it('should error with no teamsChannelId', function(done) { + const context = new TestContext(createActivityNoTeamId()); + + teamsCreateConversation(context, null, createActivityNoTeamId()) + .then(result => { + done(new Error('teamsCreateConversation() should have thrown an error')); + }) + .catch((error) => { + assert.strictEqual(error.message, 'Missing valid teamsChannelId argument'); + done(); + }); + }); + + it('should error with no activity', function(done) { + const context = new TestContext(createActivityNoTeamId()); + + teamsCreateConversation(context, 'msteams', null) + .then(result => { + done(new Error('teamsCreateConversation() should have thrown an error')); + }) + .catch((error) => { + assert.strictEqual(error.message, 'Missing valid message argument'); + done(); + }); + }); + + it('should get results from teamsCreateConversation', async function() { + const context = new TestContext(createActivityNoTeamId()); + + const result = await teamsCreateConversation(context, 'mycrazyteamschannel', createActivityNoTeamId()); + + assert(result); + assert.strictEqual(result.length, 2); + assert(result[0]); + assert(result[1]); + assert.strictEqual(result[1], 'MYACTIVITYID'); + assert.strictEqual(result[0].activityId, 1); + assert.strictEqual(result[0].conversation.id, 'MyCreationId'); + assert.strictEqual(result[0].channelId, 'teams'); + }); + }); + + describe('teamsSendToGeneralChannel()', () => { + it('should error with no teamId', function(done) { + const context = new TestContext(createActivityNoTeamId()); + + teamsSendToGeneralChannel(context, null, createActivityNoTeamId()) + .then(result => { + done(new Error('teamsSendToGeneralChannel() should have thrown an error')); + }) + .catch(error => { + assert.strictEqual(error.message, 'The current Activity was not sent from a Teams Team.'); + done(); + }); + }); + + it('should error with no activity', async function() { + const context = new TestContext(createActivityNoTeamId()); + + await teamsSendToGeneralChannel(context, 'msteams', null).catch((error) => { + assert.strictEqual(error.message, 'The current Activity was not sent from a Teams Team.'); + }); + }); + + it('should get results', async function() { + const context = new TestContext(createActivityTeamId()); + + const result = await teamsSendToGeneralChannel(context, 'mycrazyteamschannel', createActivityTeamId()); + + assert(result); + assert.strictEqual(result.length, 2); + assert(result[0]); + assert(result[1]); + assert.strictEqual(result[1], 'MYACTIVITYID'); + assert.strictEqual(result[0].activityId, 1); + assert.strictEqual(result[0].conversation.id, 'MyCreationId'); + assert.strictEqual(result[0].channelId, 'teams'); + }); + }); +}) -function createActivityNoId() { +function createActivityNoTeamId() { return { type: 'message', timestamp: Date.now,