898 lines
No EOL
38 KiB
JavaScript
898 lines
No EOL
38 KiB
JavaScript
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TestFlow = exports.TestAdapter = void 0;
|
|
/**
|
|
* @module botbuilder
|
|
*/
|
|
/**
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
// tslint:disable-next-line:no-require-imports
|
|
const assert_1 = __importDefault(require("assert"));
|
|
const uuid_1 = require("uuid");
|
|
const botframework_schema_1 = require("botframework-schema");
|
|
const botAdapter_1 = require("./botAdapter");
|
|
const turnContext_1 = require("./turnContext");
|
|
/**
|
|
* Test adapter used for unit tests. This adapter can be used to simulate sending messages from the
|
|
* user to the bot.
|
|
*
|
|
* @remarks
|
|
* The following example sets up the test adapter and then executes a simple test:
|
|
*
|
|
* ```JavaScript
|
|
* const { TestAdapter } = require('botbuilder');
|
|
*
|
|
* const adapter = new TestAdapter(async (context) => {
|
|
* await context.sendActivity(`Hello World`);
|
|
* });
|
|
*
|
|
* adapter.test(`hi`, `Hello World`)
|
|
* .then(() => done());
|
|
* ```
|
|
*/
|
|
class TestAdapter extends botAdapter_1.BotAdapter {
|
|
/**
|
|
* Creates a new TestAdapter instance.
|
|
*
|
|
* @param logicOrConversation The bots logic that's under test.
|
|
* @param template (Optional) activity containing default values to assign to all test messages received.
|
|
* @param sendTraceActivity Indicates whether the adapter should add to its queue any trace activities generated by the bot.
|
|
*/
|
|
constructor(logicOrConversation, template, sendTraceActivity = false) {
|
|
super();
|
|
/**
|
|
* Gets or sets the locale for the conversation.
|
|
*/
|
|
this.locale = 'en-us';
|
|
/**
|
|
* Gets the queue of responses from the bot.
|
|
*/
|
|
this.activeQueue = [];
|
|
this._sendTraceActivity = false;
|
|
this._nextId = 0;
|
|
this.ExceptionExpected = 'ExceptionExpected';
|
|
this._userTokens = [];
|
|
this._magicCodes = [];
|
|
this.exchangeableTokens = {};
|
|
this._sendTraceActivity = sendTraceActivity;
|
|
this.template = template || {};
|
|
if (logicOrConversation) {
|
|
if (typeof logicOrConversation === 'function') {
|
|
this._logic = logicOrConversation;
|
|
this.conversation = TestAdapter.createConversation('Convo1');
|
|
}
|
|
else {
|
|
this.conversation = logicOrConversation;
|
|
}
|
|
}
|
|
else {
|
|
this.conversation = TestAdapter.createConversation('Convo1');
|
|
}
|
|
Object.assign(this.conversation, {
|
|
locale: this.template.locale || this.conversation.locale || this.locale,
|
|
serviceUrl: this.template.serviceUrl || this.conversation.serviceUrl,
|
|
channelId: this.template.channelId || this.conversation.channelId,
|
|
bot: this.template.recipient || this.conversation.bot,
|
|
user: this.template.from || this.conversation.user,
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* INTERNAL: used to drive the promise chain forward when running tests.
|
|
*/
|
|
get activityBuffer() {
|
|
return this.activeQueue;
|
|
}
|
|
/**
|
|
* Gets a value indicating whether to send trace activities.
|
|
*
|
|
* @returns A value indicating whether to send trace activities.
|
|
*/
|
|
get enableTrace() {
|
|
return this._sendTraceActivity;
|
|
}
|
|
/**
|
|
* Sets a value inidicating whether to send trace activities.
|
|
*/
|
|
set enableTrace(value) {
|
|
this._sendTraceActivity = value;
|
|
}
|
|
/**
|
|
* Create a ConversationReference.
|
|
*
|
|
* @param name name of the conversation (also id).
|
|
* @param user name of the user (also id) default: User1.
|
|
* @param bot name of the bot (also id) default: Bot.
|
|
* @returns The [ConversationReference](xref:botframework-schema.ConversationReference).
|
|
*/
|
|
static createConversation(name, user = 'User1', bot = 'Bot') {
|
|
const conversationReference = {
|
|
channelId: botframework_schema_1.Channels.Test,
|
|
serviceUrl: 'https://test.com',
|
|
conversation: { isGroup: false, id: name, name: name },
|
|
user: { id: user.toLowerCase(), name: user },
|
|
bot: { id: bot.toLowerCase(), name: bot },
|
|
locale: 'en-us',
|
|
};
|
|
return conversationReference;
|
|
}
|
|
/**
|
|
* Dequeues and returns the next bot response from the activeQueue.
|
|
*
|
|
* @returns The next activity in the queue; or undefined, if the queue is empty.
|
|
*/
|
|
getNextReply() {
|
|
if (this.activeQueue.length > 0) {
|
|
return this.activeQueue.shift();
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* Creates a message activity from text and the current conversational context.
|
|
*
|
|
* @param text The message text.
|
|
* @returns An appropriate message activity.
|
|
*/
|
|
makeActivity(text) {
|
|
const activity = {
|
|
type: botframework_schema_1.ActivityTypes.Message,
|
|
locale: this.locale,
|
|
from: this.conversation.user,
|
|
recipient: this.conversation.bot,
|
|
conversation: this.conversation.conversation,
|
|
serviceUrl: this.conversation.serviceUrl,
|
|
id: (this._nextId++).toString(),
|
|
text: text,
|
|
};
|
|
return activity;
|
|
}
|
|
/**
|
|
* Processes a message activity from a user.
|
|
*
|
|
* @param userSays The text of the user's message.
|
|
* @param callback The bot logic to invoke.
|
|
* @returns {Promise<any>} A promise representing the async operation.
|
|
*/
|
|
sendTextToBot(userSays, callback) {
|
|
return this.processActivity(this.makeActivity(userSays), callback);
|
|
}
|
|
/**
|
|
* Receives an activity and runs it through the middleware pipeline.
|
|
*
|
|
* @param activity The activity to process.
|
|
* @param callback The bot logic to invoke.
|
|
* @returns {Promise<any>} A promise representing the async operation.
|
|
*/
|
|
processActivity(activity, callback) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const request = typeof activity === 'string' ? { type: botframework_schema_1.ActivityTypes.Message, text: activity } : activity;
|
|
request.type = request.type || botframework_schema_1.ActivityTypes.Message;
|
|
request.channelId = request.channelId || this.conversation.channelId;
|
|
if (!request.from || request.from.id === 'unknown' || request.from.role === botframework_schema_1.RoleTypes.Bot) {
|
|
request.from = this.conversation.user;
|
|
}
|
|
request.recipient = request.recipient || this.conversation.bot;
|
|
request.conversation = request.conversation || this.conversation.conversation;
|
|
request.serviceUrl = request.serviceUrl || this.conversation.serviceUrl;
|
|
request.id = request.id || (this._nextId++).toString();
|
|
request.timestamp = request.timestamp || new Date();
|
|
Object.assign(request, this.template);
|
|
const context = this.createContext(request);
|
|
if (callback) {
|
|
return yield this.runMiddleware(context, callback);
|
|
}
|
|
else if (this._logic) {
|
|
return yield this.runMiddleware(context, this._logic);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* Sends activities to the conversation.
|
|
* @param context Context object for the current turn of conversation with the user.
|
|
* @param activities Set of activities sent by logic under test.
|
|
*/
|
|
sendActivities(context, activities) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!context) {
|
|
throw new Error('TurnContext cannot be null.');
|
|
}
|
|
if (!activities) {
|
|
throw new Error('Activities cannot be null.');
|
|
}
|
|
if (activities.length == 0) {
|
|
throw new Error('Expecting one or more activities, but the array was empty.');
|
|
}
|
|
const responses = [];
|
|
for (let i = 0; i < activities.length; i++) {
|
|
const activity = activities[i];
|
|
if (!activity.id) {
|
|
activity.id = (0, uuid_1.v4)();
|
|
}
|
|
if (!activity.timestamp) {
|
|
activity.timestamp = new Date();
|
|
}
|
|
if (activity.type === 'delay') {
|
|
const delayMs = parseInt(activity.value);
|
|
yield new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
else if (activity.type === botframework_schema_1.ActivityTypes.Trace) {
|
|
if (this._sendTraceActivity) {
|
|
this.activeQueue.push(activity);
|
|
}
|
|
}
|
|
else {
|
|
this.activeQueue.push(activity);
|
|
}
|
|
responses.push({ id: activity.id });
|
|
}
|
|
return responses;
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* Replaces an existing activity in the activeQueue.
|
|
* @param context Context object for the current turn of conversation with the user.
|
|
* @param activity Activity being updated.
|
|
* @returns promise representing async operation
|
|
*/
|
|
updateActivity(context, activity) {
|
|
if (activity.id) {
|
|
const idx = this.activeQueue.findIndex((a) => a.id === activity.id);
|
|
if (idx !== -1) {
|
|
this.activeQueue.splice(idx, 1, activity);
|
|
}
|
|
return Promise.resolve({ id: activity.id });
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
/**
|
|
* @private
|
|
* Deletes an existing activity in the activeQueue.
|
|
* @param context Context object for the current turn of conversation with the user.
|
|
* @param reference `ConversationReference` for activity being deleted.
|
|
*/
|
|
deleteActivity(context, reference) {
|
|
if (reference.activityId) {
|
|
const idx = this.activeQueue.findIndex((a) => a.id === reference.activityId);
|
|
if (idx !== -1) {
|
|
this.activeQueue.splice(idx, 1);
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
/**
|
|
* @private
|
|
* INTERNAL: called by a `TestFlow` instance to simulate a user sending a message to the bot.
|
|
* This will cause the adapters middleware pipe to be run and it's logic to be called.
|
|
* @param activity Text or activity from user. The current conversation reference [template](#template) will be merged the passed in activity to properly address the activity. Fields specified in the activity override fields in the template.
|
|
*/
|
|
receiveActivity(activity) {
|
|
return this.processActivity(activity);
|
|
}
|
|
/**
|
|
* The `TestAdapter` doesn't implement `continueConversation()` and will return an error if it's
|
|
* called.
|
|
*
|
|
* @param _reference A reference to the conversation to continue.
|
|
* @param _logic The asynchronous method to call after the adapter middleware runs.
|
|
* @returns {Promise<void>} A promise representing the async operation.
|
|
*/
|
|
continueConversation(_reference, _logic) {
|
|
return Promise.reject(new Error('not implemented'));
|
|
}
|
|
/**
|
|
* Creates a turn context.
|
|
*
|
|
* @param request An incoming request body.
|
|
* @returns The created [TurnContext](xref:botbuilder-core.TurnContext).
|
|
* @remarks
|
|
* Override this in a derived class to modify how the adapter creates a turn context.
|
|
*/
|
|
createContext(request) {
|
|
return new turnContext_1.TurnContext(this, request);
|
|
}
|
|
/**
|
|
* Sends something to the bot. This returns a new `TestFlow` instance which can be used to add
|
|
* additional steps for inspecting the bots reply and then sending additional activities.
|
|
*
|
|
* @remarks
|
|
* This example shows how to send a message and then verify that the response was as expected:
|
|
*
|
|
* ```JavaScript
|
|
* adapter.send('hi')
|
|
* .assertReply('Hello World')
|
|
* .then(() => done());
|
|
* ```
|
|
* @param userSays Text or activity simulating user input.
|
|
* @returns a new [TestFlow](xref:botbuilder-core.TestFlow) instance which can be used to add additional steps
|
|
* for inspecting the bots reply and then sending additional activities.
|
|
*/
|
|
send(userSays) {
|
|
return new TestFlow(this.processActivity(userSays), this);
|
|
}
|
|
/**
|
|
* Send something to the bot and expects the bot to return with a given reply.
|
|
*
|
|
* @remarks
|
|
* This is simply a wrapper around calls to `send()` and `assertReply()`. This is such a
|
|
* common pattern that a helper is provided.
|
|
*
|
|
* ```JavaScript
|
|
* adapter.test('hi', 'Hello World')
|
|
* .then(() => done());
|
|
* ```
|
|
* @param userSays Text or activity simulating user input.
|
|
* @param expected Expected text or activity of the reply sent by the bot.
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param _timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
test(userSays, expected, description, _timeout) {
|
|
return this.send(userSays).assertReply(expected, description);
|
|
}
|
|
/**
|
|
* Test a list of activities.
|
|
*
|
|
* @remarks
|
|
* Each activity with the "bot" role will be processed with assertReply() and every other
|
|
* activity will be processed as a user message with send().
|
|
* @param activities Array of activities.
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
testActivities(activities, description, timeout) {
|
|
if (!activities) {
|
|
throw new Error('Missing array of activities');
|
|
}
|
|
const activityInspector = (expected) => (actual, description2) => validateTranscriptActivity(actual, expected, description2);
|
|
// Chain all activities in a TestFlow, check if its a user message (send) or a bot reply (assert)
|
|
return activities.reduce((flow, activity) => {
|
|
// tslint:disable-next-line:prefer-template
|
|
const assertDescription = `reply ${description ? ' from ' + description : ''}`;
|
|
return this.isReply(activity)
|
|
? flow.assertReply(activityInspector(activity, description), assertDescription, timeout)
|
|
: flow.send(activity);
|
|
}, new TestFlow(Promise.resolve(), this));
|
|
}
|
|
/**
|
|
* Adds a fake user token so it can later be retrieved.
|
|
*
|
|
* @param connectionName The connection name.
|
|
* @param channelId The channel id.
|
|
* @param userId The user id.
|
|
* @param token The token to store.
|
|
* @param magicCode (Optional) The optional magic code to associate with this token.
|
|
*/
|
|
addUserToken(connectionName, channelId, userId, token, magicCode) {
|
|
const key = new UserToken();
|
|
key.channelId = channelId;
|
|
key.connectionName = connectionName;
|
|
key.userId = userId;
|
|
key.token = token;
|
|
if (!magicCode) {
|
|
this._userTokens.push(key);
|
|
}
|
|
else {
|
|
const mc = new TokenMagicCode();
|
|
mc.key = key;
|
|
mc.magicCode = magicCode;
|
|
this._magicCodes.push(mc);
|
|
}
|
|
}
|
|
/**
|
|
* Asynchronously retrieves the token status for each configured connection for the given user.
|
|
* In testAdapter, retrieves tokens which were previously added via addUserToken.
|
|
*
|
|
* @param context The context object for the turn.
|
|
* @param userId The ID of the user to retrieve the token status for.
|
|
* @param includeFilter Optional. A comma-separated list of connection's to include. If present,
|
|
* the `includeFilter` parameter limits the tokens this method returns.
|
|
* @param _oAuthAppCredentials AppCredentials for OAuth.
|
|
* @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved.
|
|
*/
|
|
getTokenStatus(context, userId, includeFilter, _oAuthAppCredentials) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!context || !context.activity) {
|
|
throw new Error('testAdapter.getTokenStatus(): context with activity is required');
|
|
}
|
|
if (!userId && (!context.activity.from || !context.activity.from.id)) {
|
|
throw new Error('testAdapter.getTokenStatus(): missing userId, from or from.id');
|
|
}
|
|
const filter = includeFilter ? includeFilter.split(',') : undefined;
|
|
if (!userId) {
|
|
userId = context.activity.from.id;
|
|
}
|
|
return this._userTokens
|
|
.filter((x) => x.channelId === context.activity.channelId &&
|
|
x.userId === userId &&
|
|
(!filter || filter.includes(x.connectionName)))
|
|
.map((token) => ({
|
|
ConnectionName: token.connectionName,
|
|
HasToken: true,
|
|
ServiceProviderDisplayName: token.connectionName,
|
|
}));
|
|
});
|
|
}
|
|
/**
|
|
* Retrieves the OAuth token for a user that is in a sign-in flow.
|
|
*
|
|
* @param context Context for the current turn of conversation with the user.
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @param magicCode (Optional) Optional user entered code to validate.
|
|
* @returns The OAuth token for a user that is in a sign-in flow.
|
|
*/
|
|
getUserToken(context, connectionName, magicCode) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const key = new UserToken();
|
|
key.channelId = context.activity.channelId;
|
|
key.connectionName = connectionName;
|
|
key.userId = context.activity.from.id;
|
|
if (magicCode) {
|
|
const magicCodeRecord = this._magicCodes.find((item) => key.equalsKey(item.key) && item.magicCode === magicCode);
|
|
if (magicCodeRecord) {
|
|
// move the token to long term dictionary
|
|
this.addUserToken(connectionName, key.channelId, key.userId, magicCodeRecord.key.token);
|
|
// remove from the magic code list
|
|
const idx = this._magicCodes.indexOf(magicCodeRecord);
|
|
this._magicCodes.splice(idx, 1);
|
|
}
|
|
}
|
|
const userToken = this._userTokens.find((token) => key.equalsKey(token));
|
|
return userToken && Object.assign({ expiration: undefined }, userToken);
|
|
});
|
|
}
|
|
/**
|
|
* Signs the user out with the token server.
|
|
*
|
|
* @param context Context for the current turn of conversation with the user.
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @param userId User ID to sign out.
|
|
*/
|
|
signOutUser(context, connectionName, userId) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const channelId = context.activity.channelId;
|
|
userId = userId || context.activity.from.id;
|
|
this._userTokens = this._userTokens.filter((token) => connectionName &&
|
|
(connectionName !== token.connectionName || channelId !== token.channelId || userId !== token.userId));
|
|
});
|
|
}
|
|
/**
|
|
* Gets a signin link from the token server that can be sent as part of a SigninCard.
|
|
*
|
|
* @param context Context for the current turn of conversation with the user.
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @returns A signin link from the token server that can be sent as part of a SigninCard.
|
|
*/
|
|
getSignInLink(context, connectionName) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return `https://fake.com/oauthsignin/${connectionName}/${context.activity.channelId}/${context.activity.from.id}`;
|
|
});
|
|
}
|
|
/**
|
|
* Signs the user out with the token server.
|
|
*
|
|
* @param _context Context for the current turn of conversation with the user.
|
|
* @param _connectionName Name of the auth connection to use.
|
|
* @param _resourceUrls The list of resource URLs to retrieve tokens for.
|
|
* @returns A Dictionary of resourceUrl to the corresponding TokenResponse.
|
|
*/
|
|
getAadTokens(_context, _connectionName, _resourceUrls) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return undefined;
|
|
});
|
|
}
|
|
/**
|
|
* Adds a fake exchangeable token so it can be exchanged later.
|
|
*
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @param channelId Channel ID.
|
|
* @param userId User ID.
|
|
* @param exchangeableItem Exchangeable token or resource URI.
|
|
* @param token Token to store.
|
|
*/
|
|
addExchangeableToken(connectionName, channelId, userId, exchangeableItem, token) {
|
|
const key = new ExchangeableToken();
|
|
key.channelId = channelId;
|
|
key.connectionName = connectionName;
|
|
key.userId = userId;
|
|
key.exchangeableItem = exchangeableItem;
|
|
key.token = token;
|
|
this.exchangeableTokens[key.toKey()] = key;
|
|
}
|
|
/**
|
|
* Gets a sign-in resource.
|
|
*
|
|
* @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user.
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @param userId User ID
|
|
* @param _finalRedirect Final redirect URL.
|
|
* @returns A `Promise` with a new [SignInUrlResponse](xref:botframework-schema.SignInUrlResponse) object.
|
|
*/
|
|
getSignInResource(context, connectionName, userId, _finalRedirect) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return {
|
|
signInLink: `https://botframeworktestadapter.com/oauthsignin/${connectionName}/${context.activity.channelId}/${userId}`,
|
|
tokenExchangeResource: {
|
|
id: String(Math.random()),
|
|
providerId: null,
|
|
uri: `api://${connectionName}/resource`,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
/**
|
|
* Performs a token exchange operation such as for single sign-on.
|
|
*
|
|
* @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user.
|
|
* @param connectionName Name of the auth connection to use.
|
|
* @param userId User id associated with the token.
|
|
* @param tokenExchangeRequest Exchange request details, either a token to exchange or a uri to exchange.
|
|
* @returns If the promise completes, the exchanged token is returned.
|
|
*/
|
|
exchangeToken(context, connectionName, userId, tokenExchangeRequest) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const exchangeableValue = tokenExchangeRequest.token
|
|
? tokenExchangeRequest.token
|
|
: tokenExchangeRequest.uri;
|
|
const key = new ExchangeableToken();
|
|
key.channelId = context.activity.channelId;
|
|
key.connectionName = connectionName;
|
|
key.exchangeableItem = exchangeableValue;
|
|
key.userId = userId;
|
|
const tokenExchangeResponse = this.exchangeableTokens[key.toKey()];
|
|
if (tokenExchangeResponse && tokenExchangeResponse.token === this.ExceptionExpected) {
|
|
throw new Error('Exception occurred during exchanging tokens');
|
|
}
|
|
return tokenExchangeResponse
|
|
? {
|
|
channelId: key.channelId,
|
|
connectionName: key.connectionName,
|
|
token: tokenExchangeResponse.token,
|
|
expiration: null,
|
|
}
|
|
: null;
|
|
});
|
|
}
|
|
/**
|
|
* Adds an instruction to throw an exception during exchange requests.
|
|
*
|
|
* @param connectionName The connection name.
|
|
* @param channelId The channel id.
|
|
* @param userId The user id.
|
|
* @param exchangeableItem The exchangeable token or resource URI.
|
|
*/
|
|
throwOnExchangeRequest(connectionName, channelId, userId, exchangeableItem) {
|
|
const token = new ExchangeableToken();
|
|
token.channelId = channelId;
|
|
token.connectionName = connectionName;
|
|
token.userId = userId;
|
|
token.exchangeableItem = exchangeableItem;
|
|
const key = token.toKey();
|
|
token.token = this.ExceptionExpected;
|
|
this.exchangeableTokens[key] = token;
|
|
}
|
|
/**
|
|
* Indicates if the activity is a reply from the bot (role == 'bot')
|
|
*
|
|
* @remarks
|
|
* Checks to see if the from property and if from.role exists on the Activity before
|
|
* checking to see who the activity is from. Otherwise returns false by default.
|
|
* @param activity Activity to check.
|
|
* @returns True if the activity is a reply from the bot, otherwise, false.
|
|
*/
|
|
isReply(activity) {
|
|
if (activity.from && activity.from.role) {
|
|
return activity.from.role && activity.from.role.toLocaleLowerCase() === 'bot';
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
exports.TestAdapter = TestAdapter;
|
|
class UserToken {
|
|
equalsKey(rhs) {
|
|
return (rhs &&
|
|
this.connectionName === rhs.connectionName &&
|
|
this.userId === rhs.userId &&
|
|
this.channelId === rhs.channelId);
|
|
}
|
|
}
|
|
class TokenMagicCode {
|
|
}
|
|
class ExchangeableToken extends UserToken {
|
|
equalsKey(rhs) {
|
|
return rhs != null && this.exchangeableItem === rhs.exchangeableItem && super.equalsKey(rhs);
|
|
}
|
|
toKey() {
|
|
return this.exchangeableItem;
|
|
}
|
|
}
|
|
/**
|
|
* Support class for `TestAdapter` that allows for the simple construction of a sequence of tests.
|
|
*
|
|
* @remarks
|
|
* Calling `adapter.send()` or `adapter.test()` will create a new test flow which you can chain
|
|
* together additional tests using a fluent syntax.
|
|
*
|
|
* ```JavaScript
|
|
* const { TestAdapter } = require('botbuilder');
|
|
*
|
|
* const adapter = new TestAdapter(async (context) => {
|
|
* if (context.text === 'hi') {
|
|
* await context.sendActivity(`Hello World`);
|
|
* } else if (context.text === 'bye') {
|
|
* await context.sendActivity(`Goodbye`);
|
|
* }
|
|
* });
|
|
*
|
|
* adapter.test(`hi`, `Hello World`)
|
|
* .test(`bye`, `Goodbye`)
|
|
* .then(() => done());
|
|
* ```
|
|
*/
|
|
class TestFlow {
|
|
/**
|
|
* @private
|
|
* INTERNAL: creates a new TestFlow instance.
|
|
* @param previous Promise chain for the current test sequence.
|
|
* @param adapter Adapter under tested.
|
|
* @param callback The bot turn processing logic to test.
|
|
*/
|
|
constructor(previous, adapter, callback) {
|
|
this.previous = previous;
|
|
this.adapter = adapter;
|
|
this.callback = callback;
|
|
}
|
|
/**
|
|
* Send something to the bot and expects the bot to return with a given reply. This is simply a
|
|
* wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a
|
|
* helper is provided.
|
|
*
|
|
* @param userSays Text or activity simulating user input.
|
|
* @param expected Expected text or activity of the reply sent by the bot.
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
test(userSays, expected, description, timeout) {
|
|
return this.send(userSays).assertReply(expected, description || `test("${userSays}", "${expected}")`, timeout);
|
|
}
|
|
/**
|
|
* Sends something to the bot.
|
|
*
|
|
* @param userSays Text or activity simulating user input.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
send(userSays) {
|
|
return new TestFlow(this.previous.then(() => this.adapter.processActivity(userSays, this.callback)), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Creates a conversation update activity and process the activity.
|
|
*
|
|
* @returns {TestFlow} A new TestFlow object.
|
|
*/
|
|
sendConversationUpdate() {
|
|
return new TestFlow(this.previous.then(() => {
|
|
var _a;
|
|
const cu = botframework_schema_1.ActivityEx.createConversationUpdateActivity();
|
|
(_a = cu.membersAdded) !== null && _a !== void 0 ? _a : (cu.membersAdded = []);
|
|
cu.membersAdded.push(this.adapter.conversation.user);
|
|
return this.adapter.processActivity(cu, this.callback);
|
|
}), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Generates an assertion if the bots response doesn't match the expected text/activity.
|
|
*
|
|
* @param expected Expected text or activity from the bot. Can be a callback to inspect the response using custom logic.
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
assertReply(expected, description, timeout) {
|
|
function defaultInspector(reply, description2) {
|
|
if (typeof expected === 'object') {
|
|
validateActivity(reply, expected);
|
|
}
|
|
else {
|
|
assert_1.default.equal(reply.type, botframework_schema_1.ActivityTypes.Message, `${description2} type === '${reply.type}'. `);
|
|
assert_1.default.equal(reply.text, expected, `${description2} text === "${reply.text}"`);
|
|
}
|
|
}
|
|
if (!description) {
|
|
description = '';
|
|
}
|
|
const inspector = typeof expected === 'function' ? expected : defaultInspector;
|
|
return new TestFlow(this.previous.then(() => {
|
|
// tslint:disable-next-line:promise-must-complete
|
|
return new Promise((resolve, reject) => {
|
|
if (!timeout) {
|
|
timeout = 3000;
|
|
}
|
|
const start = new Date().getTime();
|
|
const adapter = this.adapter;
|
|
function waitForActivity() {
|
|
const current = new Date().getTime();
|
|
if (current - start > timeout) {
|
|
// Operation timed out
|
|
let expecting;
|
|
switch (typeof expected) {
|
|
case 'string':
|
|
default:
|
|
expecting = `"${expected.toString()}"`;
|
|
break;
|
|
case 'object':
|
|
expecting = `"${expected.text}`;
|
|
break;
|
|
case 'function':
|
|
expecting = expected.toString();
|
|
break;
|
|
}
|
|
reject(new Error(`TestAdapter.assertReply(${expecting}): ${description} Timed out after ${current - start}ms.`));
|
|
}
|
|
else if (adapter.activeQueue.length > 0) {
|
|
// Activity received
|
|
const reply = adapter.activeQueue.shift();
|
|
try {
|
|
inspector(reply, description);
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
resolve();
|
|
}
|
|
else {
|
|
setTimeout(waitForActivity, 5);
|
|
}
|
|
}
|
|
waitForActivity();
|
|
});
|
|
}), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Generates an assertion that the turn processing logic did not generate a reply from the bot, as expected.
|
|
*
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
assertNoReply(description, timeout) {
|
|
return new TestFlow(this.previous.then(() => {
|
|
// tslint:disable-next-line:promise-must-complete
|
|
return new Promise((resolve) => {
|
|
if (!timeout) {
|
|
timeout = 3000;
|
|
}
|
|
const start = new Date().getTime();
|
|
const adapter = this.adapter;
|
|
function waitForActivity() {
|
|
const current = new Date().getTime();
|
|
if (current - start > timeout) {
|
|
// Operation timed out and received no reply
|
|
resolve();
|
|
}
|
|
else if (adapter.activeQueue.length > 0) {
|
|
// Activity received
|
|
const reply = adapter.activeQueue.shift();
|
|
assert_1.default.strictEqual(reply, undefined, `${JSON.stringify(reply)} is responded when waiting for no reply: '${description}'`);
|
|
resolve();
|
|
}
|
|
else {
|
|
setTimeout(waitForActivity, 5);
|
|
}
|
|
}
|
|
waitForActivity();
|
|
});
|
|
}), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Generates an assertion if the bots response is not one of the candidate strings.
|
|
*
|
|
* @param candidates List of candidate responses.
|
|
* @param description (Optional) Description of the test case. If not provided one will be generated.
|
|
* @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
assertReplyOneOf(candidates, description, timeout) {
|
|
return this.assertReply((activity, description2) => {
|
|
for (const candidate of candidates) {
|
|
if (activity.text === candidate) {
|
|
return;
|
|
}
|
|
}
|
|
assert_1.default.fail(`TestAdapter.assertReplyOneOf(): ${description2 || ''} FAILED, Expected one of :${JSON.stringify(candidates)}`);
|
|
}, description, timeout);
|
|
}
|
|
/**
|
|
* Inserts a delay before continuing.
|
|
*
|
|
* @param ms ms to wait.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
delay(ms) {
|
|
return new TestFlow(this.previous.then(() => {
|
|
return new Promise((resolve, _reject) => {
|
|
setTimeout(resolve, ms);
|
|
});
|
|
}), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Adds a `then()` step to the tests promise chain.
|
|
*
|
|
* @param onFulfilled Code to run if the test is currently passing.
|
|
* @param onRejected Code to run if the test has thrown an error.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
then(onFulfilled, onRejected) {
|
|
return new TestFlow(this.previous.then(onFulfilled, onRejected), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Adds a finally clause. Note that you can't keep chaining afterwards.
|
|
*
|
|
* @param onFinally Code to run after the test chain.
|
|
* @returns {Promise<void>} A promise representing the async operation.
|
|
*/
|
|
finally(onFinally) {
|
|
return Promise.resolve(this.previous.finally(onFinally));
|
|
}
|
|
/**
|
|
* Adds a `catch()` clause to the tests promise chain.
|
|
*
|
|
* @param onRejected Code to run if the test has thrown an error.
|
|
* @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange.
|
|
*/
|
|
catch(onRejected) {
|
|
return new TestFlow(this.previous.catch(onRejected), this.adapter, this.callback);
|
|
}
|
|
/**
|
|
* Start the test sequence, returning a promise to await.
|
|
*
|
|
* @returns {Promise<void>} A promise representing the async operation.
|
|
*/
|
|
startTest() {
|
|
return this.previous;
|
|
}
|
|
}
|
|
exports.TestFlow = TestFlow;
|
|
/**
|
|
* @private
|
|
* @param activity an activity object to validate
|
|
* @param expected expected object to validate against
|
|
*/
|
|
function validateActivity(activity, expected) {
|
|
// tslint:disable-next-line:forin
|
|
Object.keys(expected).forEach((prop) => {
|
|
assert_1.default.equal(activity[prop], expected[prop]);
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
* Does a shallow comparison of:
|
|
* - type
|
|
* - text
|
|
* - speak
|
|
* - suggestedActions
|
|
*/
|
|
function validateTranscriptActivity(activity, expected, description) {
|
|
assert_1.default.equal(activity.type, expected.type, `failed "type" assert on ${description}`);
|
|
assert_1.default.equal(activity.text, expected.text, `failed "text" assert on ${description}`);
|
|
assert_1.default.equal(activity.speak, expected.speak, `failed "speak" assert on ${description}`);
|
|
assert_1.default.deepEqual(activity.suggestedActions, expected.suggestedActions, `failed "suggestedActions" assert on ${description}`);
|
|
}
|
|
//# sourceMappingURL=testAdapter.js.map
|