예제 #1
0
        private async Task <bool> AuthenticateUser(ITurnContext turnContext, CancellationToken cancellationToken)
        {
            bool waitingForLogin = false;

            // perform authorization check ... if already authorized, nothing happens, if not auth process is started
            if (_azureAdEnabled)
            {
                var botAdapter = (BotFrameworkAdapter)turnContext.Adapter;
                var token      = await botAdapter.GetUserTokenAsync(turnContext, _connectionName, null, cancellationToken);

                if (token == null)
                {
                    var promptSettings = new OAuthPromptSettings
                    {
                        ConnectionName = _configuration["AzureAd:ConnectionName"],
                        Text           = _configuration["AzureAd:LoginText"],
                        Title          = _configuration["AzureAd:ButtonText"],
                        Timeout        = 300000, // User has 5 minutes to login
                    };
                    var authPrompt = new OAuthPrompt(nameof(OAuthPrompt), promptSettings);
                    await authPrompt.Run(turnContext, _conversationState.CreateProperty <DialogState>("DialogState"), cancellationToken);

                    waitingForLogin = true;

                    // Save any state changes that might have occured during the turn.
                    await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);

                    await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
                }
            }

            return(waitingForLogin);
        }
        public async Task OAuthPromptBeginDialogWithWrongOptions()
        {
            await Assert.ThrowsAsync <ArgumentException>(async() =>
            {
                var prompt      = new OAuthPrompt("abc", new OAuthPromptSettings());
                var convoState  = new ConversationState(new MemoryStorage());
                var dialogState = convoState.CreateProperty <DialogState>("dialogState");

                var adapter = new TestAdapter()
                              .Use(new AutoSaveStateMiddleware(convoState));

                // Create new DialogSet.
                var dialogs = new DialogSet(dialogState);
                dialogs.Add(prompt);

                var tc = new TurnContext(adapter, new Activity()
                {
                    Type = ActivityTypes.Message, Conversation = new ConversationAccount()
                    {
                        Id = "123"
                    }, ChannelId = "test"
                });

                var dc = await dialogs.CreateContextAsync(tc);

                await prompt.BeginDialogAsync(dc, CancellationToken.None);
            });
        }
예제 #3
0
        public async Task GetUserTokenShouldReturnToken()
        {
            var oauthPromptSettings = new OAuthPromptSettings
            {
                ConnectionName = ConnectionName,
                Text           = "Please sign in",
                Title          = "Sign in",
            };

            var prompt      = new OAuthPrompt("OAuthPrompt", oauthPromptSettings);
            var convoState  = new ConversationState(new MemoryStorage());
            var dialogState = convoState.CreateProperty <DialogState>("dialogState");

            var adapter = new TestAdapter()
                          .Use(new AutoSaveStateMiddleware(convoState));

            adapter.AddUserToken(ConnectionName, ChannelId, UserId, Token);

            // Create new DialogSet.
            var dialogs = new DialogSet(dialogState);

            dialogs.Add(prompt);

            var activity = new Activity {
                ChannelId = ChannelId, From = new ChannelAccount {
                    Id = UserId
                }
            };
            var turnContext = new TurnContext(adapter, activity);

            var userToken = await prompt.GetUserTokenAsync(turnContext, CancellationToken.None);

            Assert.Equal(Token, userToken.Token);
        }
예제 #4
0
        private async Task <DialogTurnResult> DisplayTokenAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var result = (bool)stepContext.Result;

            if (result)
            {
                const string showTokenMessage = "Here is your token:";
                await stepContext.Context.SendActivityAsync(MessageFactory.Text($"{showTokenMessage} {stepContext.Values["Token"]}", showTokenMessage, InputHints.IgnoringInput), cancellationToken);
            }

            // Sign out
            const string signOutMessage = "I have signed you out.";
            var          oauthPrompt    = new OAuthPrompt(
                "SignOut",
                new OAuthPromptSettings
            {
                ConnectionName = _connectionName,
                Text           = signOutMessage,
                Title          = "Sign Out"
            });

            await oauthPrompt.SignOutUserAsync(stepContext.Context, cancellationToken);

            await stepContext.Context.SendActivityAsync(MessageFactory.Text(signOutMessage, signOutMessage, inputHint: InputHints.IgnoringInput), cancellationToken);

            return(await stepContext.EndDialogAsync(cancellationToken : cancellationToken));
        }
예제 #5
0
        private Task SendOAuthCardAsync(DialogContext dc, IMessageActivity prompt, CancellationToken cancellationToken)
        {
            var settings = new OAuthPromptSettings {
                ConnectionName = ConnectionName?.GetValue(dc.State), Title = Title?.GetValue(dc.State), Text = Text?.GetValue(dc.State)
            };

            return(OAuthPrompt.SendOAuthCardAsync(settings, dc.Context, prompt, cancellationToken));
        }
예제 #6
0
        private Task <PromptRecognizerResult <TokenResponse> > RecognizeTokenAsync(DialogContext dc, CancellationToken cancellationToken)
        {
            var settings = new OAuthPromptSettings {
                ConnectionName = ConnectionName.GetValue(dc.State)
            };

            return(OAuthPrompt.RecognizeTokenAsync(settings, dc, cancellationToken));
        }
 public async Task OAuthPromptBeginDialogWithNoDialogContext()
 {
     await Assert.ThrowsAsync <ArgumentNullException>(async() =>
     {
         var prompt = new OAuthPrompt("abc", new OAuthPromptSettings());
         await prompt.BeginDialogAsync(null);
     });
 }
        private async Task SendOAuthCardAsync(DialogContext dc, IMessageActivity prompt, CancellationToken cancellationToken)
        {
            var title = await Title.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);

            var text = await Text.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);

            var settings = new OAuthPromptSettings {
                ConnectionName = ConnectionName?.GetValue(dc.State), Title = title, Text = text
            };
            await OAuthPrompt.SendOAuthCardAsync(settings, dc.Context, prompt, cancellationToken).ConfigureAwait(false);
        }
예제 #9
0
        public async Task OAuthPromptContinueDialogWithNullDialogContext()
        {
            await Assert.ThrowsAsync <ArgumentNullException>(async() =>
            {
                var prompt      = new OAuthPrompt("abc", new OAuthPromptSettings());
                var convoState  = new ConversationState(new MemoryStorage());
                var dialogState = convoState.CreateProperty <DialogState>("dialogState");
                var dialogs     = new DialogSet(dialogState);

                dialogs.Add(prompt);

                await prompt.ContinueDialogAsync(null, CancellationToken.None);
            });
        }
예제 #10
0
        public override async Task <DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
        {
            var skillDialogOptions = (SkillDialogOptions)options;

            _skill = skillDialogOptions.MatchedSkill;

            // Set our active Skill so later methods know which Skill to use.
            dc.ActiveDialog.State[ActiveSkillStateKey] = skillDialogOptions.MatchedSkill;

            _authPrompt = new OAuthPrompt(nameof(OAuthPrompt), new OAuthPromptSettings()
            {
                ConnectionName = skillDialogOptions.MatchedSkill.AuthConnectionName,
                Title          = "Skill Authentication",
                Text           = $"Please login to access this feature.",
            });

            var parameters = new Dictionary <string, object>();

            if (skillDialogOptions.MatchedSkill.Parameters != null)
            {
                foreach (var parameter in skillDialogOptions.MatchedSkill.Parameters)
                {
                    if (skillDialogOptions.Parameters.TryGetValue(parameter, out var paramValue))
                    {
                        parameters.Add(parameter, paramValue);
                    }
                }
            }

            var skillMetadata = new SkillMetadata(
                skillDialogOptions.LuisResult,
                skillDialogOptions.LuisService,
                skillDialogOptions.MatchedSkill.Configuration,
                parameters);

            var dialogBeginEvent = new Activity(
                type: ActivityTypes.Event,
                channelId: dc.Context.Activity.ChannelId,
                from: new ChannelAccount(id: dc.Context.Activity.From.Id, name: dc.Context.Activity.From.Name),
                recipient: new ChannelAccount(id: dc.Context.Activity.Recipient.Id, name: dc.Context.Activity.Recipient.Name),
                conversation: new ConversationAccount(id: dc.Context.Activity.Conversation.Id),
                name: SkillBeginEventName,
                value: skillMetadata);

            return(await ForwardToSkill(dc, dialogBeginEvent));
        }
예제 #11
0
        public LoginDialog(
            IServiceProvider serviceProvider,
            IBotTelemetryClient telemetryClient)
            : base(nameof(LoginDialog))
        {
            _serviceProvider = serviceProvider;

            _oauthPrompt = serviceProvider.GetService <OAuthPrompt>();
            AddDialog(_oauthPrompt);

            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                PromptStepAsync,
                LoginStepAsync
            }));

            InitialDialogId = nameof(WaterfallDialog);
        }
예제 #12
0
        private async Task SendOAuthCardAsync(DialogContext dc, IMessageActivity prompt, CancellationToken cancellationToken)
        {
            // Save state prior to sending OAuthCard: the invoke response for a token exchange from the root bot could come in
            // before this method ends or could land in another instance in scale-out scenarios, which means that if the state is not saved,
            // the OAuthInput would not be at the top of the stack, and the token exchange invoke would get discarded.
            await dc.Context.TurnState.Get <ConversationState>().SaveChangesAsync(dc.Context, false, cancellationToken).ConfigureAwait(false);

            // Prepare OAuthCard
            var title = Title == null ? null : await Title.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);

            var text = Text == null ? null : await Text.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);

            var settings = new OAuthPromptSettings {
                ConnectionName = ConnectionName?.GetValue(dc.State), Title = title, Text = text
            };

            // Send OAuthCard to root bot. The root bot could attempt to do a token exchange or if it cannot do token exchange for this connection
            // it will let the card get to the user to allow them to sign in.
            await OAuthPrompt.SendOAuthCardAsync(settings, dc.Context, prompt, cancellationToken).ConfigureAwait(false);
        }
예제 #13
0
 protected override async Task OnUnrecognizedActivityTypeAsync(ITurnContext turnContext, CancellationToken cancellationToken)
 {
     if (turnContext.Activity.Type == "invoke")
     {
         var promptSettings = new OAuthPromptSettings
         {
             ConnectionName = _configuration["AzureAd:ConnectionName"],
             Text           = _configuration["AzureAd:LoginText"],
             Title          = _configuration["AzureAd:ButtonText"],
             Timeout        = 300000, // User has 5 minutes to login
         };
         var authPrompt = new OAuthPrompt(nameof(OAuthPrompt), promptSettings);
         await authPrompt.Run(turnContext, _conversationState.CreateProperty <DialogState>("DialogState"), cancellationToken);
         await CompleteSignIn(turnContext, cancellationToken);
     }
     else
     {
         await base.OnUnrecognizedActivityTypeAsync(turnContext, cancellationToken);
     }
 }
예제 #14
0
        public MainDialog(
            IServiceProvider serviceProvider,
            IBotTelemetryClient telemetryClient)
            : base(nameof(MainDialog), serviceProvider, telemetryClient)
        {
            _services       = serviceProvider.GetService <BotServices>();
            _settings       = serviceProvider.GetService <BotSettings>();
            _templateEngine = serviceProvider.GetService <LocaleTemplateEngineManager>();
            _skillsConfig   = serviceProvider.GetService <SkillsConfiguration>();

            var userState = serviceProvider.GetService <UserState>();

            _userProfileState = userState.CreateProperty <UserProfileState>(nameof(UserProfileState));
            var conversationState = serviceProvider.GetService <ConversationState>();

            _previousResponseAccessor = conversationState.CreateProperty <List <Activity> >(StateProperties.PreviousBotResponse);

            WaterfallStep[] steps = SetupWaterfallSteps();

            AddDialog(new WaterfallDialog(nameof(MainDialog), steps));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new ChoicePrompt(nameof(ChoicePrompt))
            {
                Style = ListStyle.HeroCard
            });
            InitialDialogId = nameof(MainDialog);

            // Register dialogs
            _oauthPrompt              = serviceProvider.GetService <OAuthPrompt>();
            _loginDialog              = serviceProvider.GetService <LoginDialog>();
            _onboardingDialog         = serviceProvider.GetService <OnboardingDialog>();
            _switchSkillDialog        = serviceProvider.GetService <SwitchSkillDialog>();
            _escalateDialog           = serviceProvider.GetService <EscalateDialog>();
            _cancelDialog             = serviceProvider.GetService <CancelDialog>();
            _entertainDialog          = serviceProvider.GetService <EntertainDialog>();
            _chitchatdialog           = serviceProvider.GetService <ChitchatDialog>();
            _stressDialog             = serviceProvider.GetService <StressDialog>();
            _highstresshandlingDialog = serviceProvider.GetService <HighStressHandlingDialog>();
            _stresshandlingDialog     = serviceProvider.GetService <StressHandlingDialog>();
            _journalingDialog         = serviceProvider.GetService <JournalingDialog>();

            AddDialog(_oauthPrompt);
            AddDialog(_loginDialog);
            AddDialog(_onboardingDialog);
            AddDialog(_switchSkillDialog);
            AddDialog(_escalateDialog);
            AddDialog(_cancelDialog);
            AddDialog(_entertainDialog);
            AddDialog(_chitchatdialog);
            AddDialog(_stressDialog);
            AddDialog(_highstresshandlingDialog);
            AddDialog(_stresshandlingDialog);
            AddDialog(_journalingDialog);



            // Register a QnAMakerDialog for each registered knowledgebase and ensure localised responses are provided.
            var localizedServices = _services.GetCognitiveModels();

            foreach (var knowledgebase in localizedServices.QnAConfiguration)
            {
                var qnaDialog = new QnAMakerDialog(
                    knowledgeBaseId: knowledgebase.Value.KnowledgeBaseId,
                    endpointKey: knowledgebase.Value.EndpointKey,
                    hostName: knowledgebase.Value.Host,
                    noAnswer: _templateEngine.GenerateActivityForLocale("UnsupportedMessage"),
                    activeLearningCardTitle: _templateEngine.GenerateActivityForLocale("QnaMakerAdaptiveLearningCardTitle").Text,
                    cardNoMatchText: _templateEngine.GenerateActivityForLocale("QnaMakerNoMatchText").Text)
                {
                    Id = knowledgebase.Key
                };
                AddDialog(qnaDialog);
            }

            // Register skill dialogs
            var skillDialogs = serviceProvider.GetServices <SkillDialog>();

            foreach (var dialog in skillDialogs)
            {
                AddDialog(dialog);
            }
        }
예제 #15
0
 public void OAuthPromptWithEmptyIdShouldFail()
 {
     var emptyId       = string.Empty;
     var confirmPrompt = new OAuthPrompt(emptyId, new OAuthPromptSettings());
 }
예제 #16
0
 public void OAuthPromptWithEmptySettingsShouldFail()
 {
     var confirmPrompt = new OAuthPrompt("abc", null);
 }
예제 #17
0
        /// <summary>
        /// Called when a prompt dialog is pushed onto the dialog stack and is being activated.
        /// </summary>
        /// <param name="dc">The dialog context for the current turn of the conversation.</param>
        /// <param name="options">Optional, additional information to pass to the prompt being started.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
        /// or threads to receive notice of cancellation.</param>
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
        /// <remarks>If the task is successful, the result indicates whether the prompt is still
        /// active after the turn has been processed by the prompt.</remarks>
        public override async Task <DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (dc == null)
            {
                throw new ArgumentNullException(nameof(dc));
            }

            if (options is CancellationToken)
            {
                throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
            }

            if (this.Disabled != null && this.Disabled.GetValue(dc.State))
            {
                return(await dc.EndDialogAsync(cancellationToken : cancellationToken).ConfigureAwait(false));
            }

            PromptOptions opt = null;

            if (options != null)
            {
                if (options is PromptOptions)
                {
                    // Ensure prompts have input hint set
                    opt = options as PromptOptions;
                    if (opt.Prompt != null && string.IsNullOrEmpty(opt.Prompt.InputHint))
                    {
                        opt.Prompt.InputHint = InputHints.AcceptingInput;
                    }

                    if (opt.RetryPrompt != null && string.IsNullOrEmpty(opt.RetryPrompt.InputHint))
                    {
                        opt.RetryPrompt.InputHint = InputHints.AcceptingInput;
                    }
                }
            }

            var op = OnInitializeOptions(dc, options);

            dc.State.SetValue(ThisPath.Options, op);
            dc.State.SetValue(TURN_COUNT_PROPERTY, 0);

            // If AlwaysPrompt is set to true, then clear Property value for turn 0.
            if (this.Property != null && this.AlwaysPrompt != null && this.AlwaysPrompt.GetValue(dc.State))
            {
                dc.State.SetValue(this.Property.GetValue(dc.State), null);
            }

            // Initialize state
            var state = dc.ActiveDialog.State;

            state[PersistedOptions] = opt;
            state[PersistedState]   = new Dictionary <string, object>
            {
                { AttemptCountKey, 0 },
            };

            state[PersistedExpires] = DateTime.UtcNow.AddMilliseconds(Timeout.GetValue(dc.State));
            OAuthPrompt.SetCallerInfoInDialogState(state, dc.Context);

            // Attempt to get the users token
            if (!(dc.Context.Adapter is IUserTokenProvider adapter))
            {
                throw new InvalidOperationException("OAuthPrompt.Recognize(): not supported by the current adapter");
            }

            var output = await adapter.GetUserTokenAsync(dc.Context, ConnectionName.GetValue(dc.State), null, cancellationToken).ConfigureAwait(false);

            if (output != null)
            {
                if (this.Property != null)
                {
                    dc.State.SetValue(this.Property.GetValue(dc.State), output);
                }

                // Return token
                return(await dc.EndDialogAsync(output, cancellationToken).ConfigureAwait(false));
            }
            else
            {
                dc.State.SetValue(TURN_COUNT_PROPERTY, 1);

                // Prompt user to login
                await SendOAuthCardAsync(dc, opt?.Prompt, cancellationToken).ConfigureAwait(false);

                return(Dialog.EndOfTurn);
            }
        }
예제 #18
0
 public async Task OAuthPromptBeginDialogWithNoDialogContext()
 {
     var prompt = new OAuthPrompt("abc", new OAuthPromptSettings());
     await prompt.BeginDialogAsync(null);
 }
예제 #19
0
        private async Task <DialogTurnResult> ForwardActivity(DialogContext dc, Activity activity)
        {
            if (!skillInitialized)
            {
                InititializeSkill(dc);
            }

            // Process the activity (pass through middleware) and then perform Skill processing
            inProcAdapter.ProcessActivity(activity, async(skillContext, cancellationToken) =>
            {
                await activatedSkill.OnTurnAsync(skillContext);
            }).Wait();

            // Incurs a lock each time but given we need to inspect each one for EOC and filter them out this saves another collection of activities. Swings and roundabouts
            var filteredActivities = new List <Activity>();
            var endOfConversation  = false;

            var replyActivity = inProcAdapter.GetNextReply();

            while (replyActivity != null)
            {
                if (replyActivity.Type == ActivityTypes.EndOfConversation)
                {
                    endOfConversation = true;
                }
                else if (replyActivity.Type == ActivityTypes.Event && replyActivity.Name == TokenRequestEventName)
                {
                    // Send trace to emulator
                    await dc.Context.SendActivityAsync(new Activity(type : ActivityTypes.Trace, text : $"<--Received a Token Request from a skill"));

                    var skill = dc.ActiveDialog.State[ActiveSkillStateKey] as SkillRegistration;

                    // If you want to force signin interactively and not cached uncomment this
                    // var a = dialogContext.Context.Adapter as BotFrameworkAdapter;
                    // await a.SignOutUserAsync(dialogContext.Context, skill.AuthConnectionName, default(CancellationToken));

                    // Skills could support multiple token types, only allow one through config for now.
                    var prompt = new OAuthPrompt(
                        "SkillAuth",
                        new OAuthPromptSettings()
                    {
                        ConnectionName = skill.AuthConnectionName,
                        Text           = $"Please signin to provide an authentication token for the {skill.Name} skill",
                        Title          = "Skill Authentication",
                        Timeout        = 300000, // User has 5 minutes to login
                    },
                        AuthPromptValidator);

                    var tokenResponse = await prompt.GetUserTokenAsync(dc.Context);

                    if (tokenResponse != null)
                    {
                        var response = replyActivity.CreateReply();
                        response.Type  = ActivityTypes.Event;
                        response.Name  = TokenResponseEventName;
                        response.Value = tokenResponse;

                        var result = await ForwardActivity(dc, response);

                        if (result.Status == DialogTurnStatus.Complete)
                        {
                            endOfConversation = true;
                        }
                    }
                    else
                    {
                        var dtr = await prompt.BeginDialogAsync(dc);
                    }
                }
                else
                {
                    filteredActivities.Add(replyActivity);
                }

                replyActivity = inProcAdapter.GetNextReply();
            }

            if (filteredActivities.Count > 0)
            {
                await dc.Context.SendActivitiesAsync(filteredActivities.ToArray());
            }

            // If we got an End of Conversation then close this skill dialog down
            if (endOfConversation)
            {
                var state = dc.Context.TurnState;
                state[ActiveSkillStateKey] = null;
                skillInitialized           = false;

                // Send trace to emulator
                await dc.Context.SendActivityAsync(new Activity(type : ActivityTypes.Trace, text : $"<--Ending the skill conversation"));

                return(await dc.EndDialogAsync());
            }

            return(EndOfTurn);
        }
예제 #20
0
        public async Task OAuthPromptSignOutUser()
        {
            // Arrange
            var userId         = "user-id";
            var connectionName = "connection-name";
            var channelId      = "channel-id";

            // Arrange the Adapter.
            var mockConnectorFactory = new Mock <ConnectorFactory>();

            mockConnectorFactory.Setup(
                x => x.CreateAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .ReturnsAsync(new ConnectorClient(new Uri("http://tempuri/")));

            var mockUserTokenClient = new Mock <UserTokenClient>();

            mockUserTokenClient.Setup(
                x => x.SignOutUserAsync(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()));

            var authenticateRequestResult = new AuthenticateRequestResult
            {
                CallerId         = "callerId",
                ClaimsIdentity   = new ClaimsIdentity(),
                ConnectorFactory = mockConnectorFactory.Object,
            };

            var mockBotFrameworkAuthentication = new Mock <BotFrameworkAuthentication>();

            mockBotFrameworkAuthentication.Setup(
                x => x.AuthenticateRequestAsync(It.IsAny <Activity>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .ReturnsAsync(authenticateRequestResult);
            mockBotFrameworkAuthentication.Setup(
                x => x.CreateUserTokenClientAsync(It.IsAny <ClaimsIdentity>(), It.IsAny <CancellationToken>()))
            .ReturnsAsync(mockUserTokenClient.Object);

            var adapter = new TestCloudAdapter(mockBotFrameworkAuthentication.Object);

            // Add the OAuthPrompt.
            var oauthPromptSettings = new OAuthPromptSettings
            {
                Text           = "Please sign in",
                ConnectionName = connectionName,
                Title          = "Sign in",
            };

            // The on-turn callback.
            BotCallbackHandler callback = async(turnContext, cancellationToken) =>
            {
                var oauthPrompt = new OAuthPrompt("OAuthPrompt", oauthPromptSettings);
                await oauthPrompt.SignOutUserAsync(turnContext, cancellationToken);
            };

            // The Activity for the turn.
            var activity = new Activity
            {
                Type = ActivityTypes.Message,
                Name = SignInConstants.TokenExchangeOperationName,
                From = new ChannelAccount {
                    Id = userId
                },
                Conversation = new ConversationAccount {
                    Id = "conversation-id"
                },
                ChannelId = channelId,
                Text      = "logout",
            };

            // Act
            var invokeResponse = await adapter.ProcessAsync(string.Empty, activity, callback);

            // Assert
            mockUserTokenClient.Verify(
                x => x.SignOutUserAsync(It.Is <string>(s => s == userId), It.Is <string>(s => s == connectionName), It.Is <string>(s => s == channelId), It.IsAny <CancellationToken>()), Times.Once());
        }