diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index 413900c235..103f2f78b2 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -10,6 +10,7 @@ import { ActivityHandlerBase } from 'botbuilder-core'; import { AppBasedLinkQuery } from 'botbuilder-core'; import { AppCredentials } from 'botframework-connector'; import { AttachmentData } from 'botbuilder-core'; +import { AuthenticateRequestResult } from 'botframework-connector'; import { AuthenticationConfiguration } from 'botframework-connector'; import { BatchFailedEntriesResponse } from 'botbuilder-core'; import { BatchOperationResponse } from 'botbuilder-core'; @@ -244,6 +245,7 @@ export class CloudAdapter extends CloudAdapterBase implements BotFrameworkHttpAd connectNamedPipe(pipeName: string, logic: (context: TurnContext) => Promise, appId: string, audience: string, callerId?: string, retryCount?: number): Promise; process(req: Request_2, res: Response_2, logic: (context: TurnContext) => Promise): Promise; process(req: Request_2, socket: INodeSocket, head: INodeBuffer, logic: (context: TurnContext) => Promise): Promise; + processActivityDirect(authorization: string | AuthenticateRequestResult, activity: Activity, logic: (context: TurnContext) => Promise): Promise; } // Warning: (ae-forgotten-export) The symbol "CloudChannelServiceHandler" needs to be exported by the entry point index.d.ts diff --git a/libraries/botbuilder/src/botFrameworkAdapter.ts b/libraries/botbuilder/src/botFrameworkAdapter.ts index 393837b85f..fb68810a4a 100644 --- a/libraries/botbuilder/src/botFrameworkAdapter.ts +++ b/libraries/botbuilder/src/botFrameworkAdapter.ts @@ -1280,6 +1280,8 @@ export class BotFrameworkAdapter /** * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. * + * Use [CloudAdapter.processActivityDirect] instead. + * * @param activity The activity to process. * @param logic The function to call at the end of the middleware pipeline. * diff --git a/libraries/botbuilder/src/cloudAdapter.ts b/libraries/botbuilder/src/cloudAdapter.ts index 90f2212ae4..b31f3f41bb 100644 --- a/libraries/botbuilder/src/cloudAdapter.ts +++ b/libraries/botbuilder/src/cloudAdapter.ts @@ -133,7 +133,6 @@ export class CloudAdapter extends CloudAdapterBase implements BotFrameworkHttpAd } const authHeader = z.string().parse(req.headers.Authorization ?? req.headers.authorization ?? ''); - try { const invokeResponse = await this.processActivity(authHeader, activity, logic); return end(invokeResponse?.status ?? StatusCodes.OK, invokeResponse?.body); @@ -146,6 +145,28 @@ export class CloudAdapter extends CloudAdapterBase implements BotFrameworkHttpAd } } + /** + * Asynchronously process an activity running the provided logic function. + * + * @param authorization The authorization header in the format: "Bearer [longString]" or the AuthenticateRequestResult for this turn. + * @param activity The activity to process. + * @param logic The logic function to apply. + * @returns a promise representing the asynchronous operation. + */ + async processActivityDirect( + authorization: string | AuthenticateRequestResult, + activity: Activity, + logic: (context: TurnContext) => Promise + ): Promise { + try { + typeof authorization === 'string' + ? await this.processActivity(authorization, activity, logic) + : await this.processActivity(authorization, activity, logic); + } catch (err) { + throw new Error(`CloudAdapter.processActivityDirect(): ERROR\n ${err.stack}`); + } + } + /** * Used to connect the adapter to a named pipe. * diff --git a/libraries/botbuilder/tests/cloudAdapter.test.js b/libraries/botbuilder/tests/cloudAdapter.test.js index 4ac6fe6a6b..7ac885e3cf 100644 --- a/libraries/botbuilder/tests/cloudAdapter.test.js +++ b/libraries/botbuilder/tests/cloudAdapter.test.js @@ -8,11 +8,13 @@ const { expect } = require('chai'); const sinon = require('sinon'); const { AuthenticationConfiguration, + AuthenticationConstants, BotFrameworkAuthenticationFactory, allowedCallersClaimsValidator, } = require('botframework-connector'); const { CloudAdapter, + ConfigurationBotFrameworkAuthentication, ConfigurationServiceClientCredentialFactory, ActivityTypes, createBotFrameworkAuthenticationFromConfiguration, @@ -20,6 +22,7 @@ const { } = require('..'); const { NamedPipeServer } = require('botframework-streaming'); const { StatusCodes } = require('botframework-schema'); +const { CallerIdConstants } = require('../../botbuilder-core/lib/index'); const FakeBuffer = () => Buffer.from([]); const FakeNodeSocket = () => new net.Socket(); @@ -54,6 +57,34 @@ describe('CloudAdapter', function () { }); describe('process', function () { + class TestConfiguration { + static DefaultConfig = { + // [AuthenticationConstants.ChannelService]: undefined, + ValidateAuthority: true, + ToChannelFromBotLoginUrl: AuthenticationConstants.ToChannelFromBotLoginUrl, + ToChannelFromBotOAuthScope: AuthenticationConstants.ToChannelFromBotOAuthScope, + ToBotFromChannelTokenIssuer: AuthenticationConstants.ToBotFromChannelTokenIssuer, + ToBotFromEmulatorOpenIdMetadataUrl: AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl, + CallerId: CallerIdConstants.PublicAzureChannel, + ToBotFromChannelOpenIdMetadataUrl: AuthenticationConstants.ToBotFromChannelOpenIdMetadataUrl, + OAuthUrl: AuthenticationConstants.OAuthUrl, + // [AuthenticationConstants.OAuthUrlKey]: 'test', + [AuthenticationConstants.BotOpenIdMetadataKey]: null, + }; + + constructor(config = {}) { + this.configuration = Object.assign({}, TestConfiguration.DefaultConfig, config); + } + + get(_path) { + return this.configuration; + } + + set(_path, _val) {} + } + + const activity = { type: ActivityTypes.Invoke, value: 'invoke' }; + const authorization = 'Bearer Authorization'; it('delegates to connect', async function () { const req = {}; const socket = FakeNodeSocket(); @@ -70,9 +101,6 @@ describe('CloudAdapter', function () { }); it('delegates to processActivity', async function () { - const authorization = 'Bearer Authorization'; - const activity = { type: ActivityTypes.Invoke, value: 'invoke' }; - const req = httpMocks.createRequest({ method: 'POST', headers: { authorization }, @@ -152,6 +180,78 @@ describe('CloudAdapter', function () { assert.equal(StatusCodes.UNAUTHORIZED, res.statusCode); expect(consoleStub.calledWithMatch({ message: 'The token has expired' })).to.be.true; }); + + it('calls processActivityDirect with string authorization', async function () { + const logic = async (context) => { + context.turnState.set(INVOKE_RESPONSE_KEY, { + type: ActivityTypes.InvokeResponse, + value: { + status: 200, + body: 'invokeResponse', + }, + }); + }; + + const mock = sandbox.mock(adapter); + mock.expects('processActivity').withArgs(authorization, activity, logic).once().resolves(); + mock.expects('connect').never(); + + await adapter.processActivityDirect(authorization, activity, logic); + + mock.verify(); + }); + + it('calls processActivityDirect with AuthenticateRequestResult authorization', async function () { + const claimsIdentity = adapter.createClaimsIdentity('appId'); + const audience = AuthenticationConstants.ToChannelFromBotOAuthScope; + const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( + TestConfiguration.DefaultConfig + ); + const connectorFactory = botFrameworkAuthentication.createConnectorFactory(claimsIdentity); + //AuthenticateRequestResult + const authentication = { + audience: audience, + claimsIdentity: claimsIdentity, + callerId: 'callerdId', + connectorFactory: connectorFactory, + }; + const logic = async (context) => { + context.turnState.set(INVOKE_RESPONSE_KEY, { + type: ActivityTypes.InvokeResponse, + value: { + status: 200, + body: 'invokeResponse', + }, + }); + }; + + const mock = sandbox.mock(adapter); + mock.expects('processActivity').withArgs(authentication, activity, logic).once().resolves(); + mock.expects('connect').never(); + + await adapter.processActivityDirect(authentication, activity, logic); + + mock.verify(); + }); + + it('calls processActivityDirect with error', async function () { + const logic = async (context) => { + context.turnState.set(INVOKE_RESPONSE_KEY, { + type: ActivityTypes.InvokeResponse, + value: { + status: 200, + body: 'invokeResponse', + }, + }); + }; + + sandbox.stub(adapter, 'processActivity').throws({ stack: 'error stack' }); + + await assert.rejects( + adapter.processActivityDirect(authorization, activity, logic), + new Error('CloudAdapter.processActivityDirect(): ERROR\n error stack') + ); + }); }); describe('connectNamedPipe', function () {