Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allows Inspection to attach anywhere within a Teams Team #1434

Merged
merged 3 commits into from
Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions libraries/botbuilder/src/inspectionMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { MicrosoftAppCredentials, ConnectorClient } from 'botframework-connector';
import { Activity, ActivityTypes, Middleware, TurnContext, BotState, ConversationReference, StatePropertyAccessor, UserState, ConversationState, Storage } from 'botbuilder-core';
import { teamsGetTeamId } from './teamsActivityHelpers';

/** @private */
class TraceActivity {
Expand Down Expand Up @@ -279,8 +280,7 @@ export class InspectionMiddleware extends InterceptionMiddleware {

private async processAttachCommand(turnContext: TurnContext, sessionId: string): Promise<any> {
var sessions = await this.inspectionStateAccessor.get(turnContext, InspectionSessionsByStatus.DefaultValue);

if (this.attachCommand(turnContext.activity.conversation.id, sessions, sessionId)) {
if (this.attachCommand(this.getAttachId(turnContext.activity), sessions, sessionId)) {
await turnContext.sendActivity('Attached to session, all traffic is being replicated for inspection.');
}
else {
Expand All @@ -306,22 +306,20 @@ export class InspectionMiddleware extends InterceptionMiddleware {
return sessionId;
}

private attachCommand(conversationId: string, sessions: InspectionSessionsByStatus, sessionId: string): boolean {

private attachCommand(attachId: string, sessions: InspectionSessionsByStatus, sessionId: string): boolean {
var inspectionSessionState = sessions.openedSessions[sessionId];
if (inspectionSessionState !== undefined) {
sessions.attachedSessions[conversationId] = inspectionSessionState;
sessions.attachedSessions[attachId] = inspectionSessionState;
delete sessions.openedSessions[sessionId];
return true;
}

return false;
}

private async findSession(turnContext: TurnContext): Promise<any> {
var sessions = await this.inspectionStateAccessor.get(turnContext, InspectionSessionsByStatus.DefaultValue);

var conversationReference = sessions.attachedSessions[turnContext.activity.conversation.id];
var conversationReference = sessions.attachedSessions[this.getAttachId(turnContext.activity)];
if (conversationReference !== undefined) {
return new InspectionSession(conversationReference, this.credentials);
}
Expand All @@ -342,9 +340,16 @@ export class InspectionMiddleware extends InterceptionMiddleware {
private async cleanUpSession(turnContext: TurnContext): Promise<any> {
var sessions = await this.inspectionStateAccessor.get(turnContext, InspectionSessionsByStatus.DefaultValue);

delete sessions.attachedSessions[turnContext.activity.conversation.id];
delete sessions.attachedSessions[this.getAttachId(turnContext.activity)];
await this.inspectionState.saveChanges(turnContext, false);
}

private getAttachId(activity: Activity) : string {
// If we are running in a Microsoft Teams Team the conversation Id will reflect a particular thread the bot is in.
// So if we are in a Team then we will associate the "attach" with the Team Id rather than the more restrictive conversation Id.
const teamId = teamsGetTeamId(activity);
return teamId ? teamId : activity.conversation.id;
}
}

/** @private */
Expand Down
82 changes: 82 additions & 0 deletions libraries/botbuilder/tests/inspectionMiddleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,88 @@ describe('InspectionMiddleware', function() {

// verify that all our expectations have been met

assert(inboundExpectation.isDone(), 'The expectation of a trace message for the inbound activity was not met');
assert(outboundExpectation.isDone(), 'The expectation of a trace message for the outbound activity was not met');
assert(stateExpectation.isDone(), 'The expectation of a trace message for the bot state was not met');
});
it('should replicate activity data to listening emulator following open and attach within Teams Team', async function() {

// set up our expectations in nock - each corresponds to a trace message we expect to receive in the emulator

const inboundExpectation = nock('https://test.com')
.post('/v3/conversations/Convo1/activities', activity => activity.type === 'trace'
&& activity.value.text == 'hi')
.reply(200, { id: 'test' });

const outboundExpectation = nock('https://test.com')
.post('/v3/conversations/Convo1/activities', activity => activity.type === 'trace'
&& activity.value.text == 'echo: hi')
.reply(200, { id: 'test' });

const stateExpectation = nock('https://test.com')
.post('/v3/conversations/Convo1/activities', activity => activity.type === 'trace'
&& activity.value.userState && activity.value.userState.x.property == 'hello'
&& activity.value.conversationState && activity.value.conversationState.y.property == 'world')
.reply(200, { id: 'test' });

// create the various storage and middleware objects we will be using

var storage = new MemoryStorage();
var inspectionState = new InspectionState(storage);
var userState = new UserState(storage);
var conversationState = new ConversationState(storage);
var inspectionMiddleware = new InspectionMiddleware(inspectionState, userState, conversationState);

// the emulator sends an /INSPECT open command - we can use another adapter here

var openActivity = MessageFactory.text('/INSPECT open');

const inspectionAdapter = new TestAdapter(async (turnContext) => {
await inspectionMiddleware.processCommand(turnContext);
}, null, true);

await inspectionAdapter.receiveActivity(openActivity);

var inspectionOpenResultActivity = inspectionAdapter.activityBuffer[0];
var attachCommand = inspectionOpenResultActivity.value;

// the logic of teh bot including replying with a message and updating user and conversation state

var x = userState.createProperty('x');
var y = conversationState.createProperty('y');

var applicationAdapter = new TestAdapter(async (turnContext) => {

await turnContext.sendActivity(MessageFactory.text(`echo: ${ turnContext.activity.text }`));

(await x.get(turnContext, { property: '' })).property = 'hello';
(await y.get(turnContext, { property: '' })).property = 'world';

await userState.saveChanges(turnContext);
await conversationState.saveChanges(turnContext);

}, null, true);

// IMPORTANT add the InspectionMiddleware to the adapter that is running our bot

applicationAdapter.use(inspectionMiddleware);

var attachActivity = MessageFactory.text(attachCommand);
attachActivity.channelData = { team: { id: 'team-id' } };

await applicationAdapter.receiveActivity(attachActivity);

// the attach command response is a informational message

var hiActivity = MessageFactory.text('hi');
hiActivity.channelData = { team: { id: 'team-id' } };

await applicationAdapter.receiveActivity(hiActivity);

// trace activities should be sent to the emulator using the connector and the conversation reference

// verify that all our expectations have been met

assert(inboundExpectation.isDone(), 'The expectation of a trace message for the inbound activity was not met');
assert(outboundExpectation.isDone(), 'The expectation of a trace message for the outbound activity was not met');
assert(stateExpectation.isDone(), 'The expectation of a trace message for the bot state was not met');
Expand Down