diff --git a/libraries/Microsoft.Bot.Builder/Adapters/TestAdapter.cs b/libraries/Microsoft.Bot.Builder/Adapters/TestAdapter.cs index 83fd247e31..6c952f3bfd 100644 --- a/libraries/Microsoft.Bot.Builder/Adapters/TestAdapter.cs +++ b/libraries/Microsoft.Bot.Builder/Adapters/TestAdapter.cs @@ -188,7 +188,7 @@ public async Task ProcessActivityAsync(Activity activity, BotCallbackHandler cal activity.LocalTimestamp = DateTimeOffset.Now; } - using (var context = new TurnContext(this, activity)) + using (var context = CreateTurnContext(activity)) { await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false); } @@ -373,7 +373,7 @@ public Task CreateConversationAsync(string channelId, BotCallbackHandler callbac var update = Activity.CreateConversationUpdateActivity(); update.ChannelId = channelId; update.Conversation = new ConversationAccount { Id = Guid.NewGuid().ToString("n") }; - using (var context = new TurnContext(this, (Activity)update)) + using (var context = CreateTurnContext((Activity)update)) { return callback(context, cancellationToken); } @@ -589,11 +589,9 @@ public virtual Task GetUserTokenAsync(ITurnContext turnContext, A Token = token, }); } - else - { - // not found - return Task.FromResult(null); - } + + // not found + return Task.FromResult(null); } /// Attempts to retrieve the token for a user that's in a login flow, using the bot's AppCredentials. @@ -883,6 +881,16 @@ public Task ExchangeTokenAsync(ITurnContext turnContext, AppCrede return Task.FromResult(null); } } + + /// + /// Creates the turn context for the adapter. + /// + /// An instance for the turn. + /// A instance to be used by the adapter. + protected virtual TurnContext CreateTurnContext(Activity activity) + { + return new TurnContext(this, activity); + } private void Enqueue(Activity activity) { diff --git a/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs b/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs index 81456e64c9..30187cd0b9 100644 --- a/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs +++ b/libraries/Microsoft.Bot.Builder/ShowTypingMiddleware.cs @@ -63,8 +63,8 @@ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, Cance Task typingTask = null; try { - // If the incoming activity is a MessageActivity, start a timer to periodically send the typing activity. - if (IsNotRunningAsSkill(turnContext) && turnContext.Activity.Type == ActivityTypes.Message) + // Start a timer to periodically send the typing activity (bots running as skills should not send typing activity) + if (!IsSkillBot(turnContext) && turnContext.Activity.Type == ActivityTypes.Message) { // do not await task - we want this to run in the background and we will cancel it when its done typingTask = SendTypingAsync(turnContext, _delay, _period, cts.Token); @@ -83,10 +83,10 @@ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, Cance } } - private static bool IsNotRunningAsSkill(ITurnContext turnContext) + private static bool IsSkillBot(ITurnContext turnContext) { return turnContext.TurnState.Get(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity - && SkillValidation.IsSkillClaim(claimIdentity.Claims) == false; + && SkillValidation.IsSkillClaim(claimIdentity.Claims); } private static async Task SendTypingAsync(ITurnContext turnContext, TimeSpan delay, TimeSpan period, CancellationToken cancellationToken) diff --git a/tests/Microsoft.Bot.Builder.Tests/ShowTypingMiddlewareTests.cs b/tests/Microsoft.Bot.Builder.Tests/ShowTypingMiddlewareTests.cs new file mode 100644 index 0000000000..385d0ba467 --- /dev/null +++ b/tests/Microsoft.Bot.Builder.Tests/ShowTypingMiddlewareTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Xunit; + +namespace Microsoft.Bot.Builder.Tests +{ + public class ShowTypingMiddlewareTests + { + [Fact] + public void ConstructorValidation() + { + Assert.Throws(() => new ShowTypingMiddleware(-100, 1000)); + Assert.Throws(() => new ShowTypingMiddleware(100, -1000)); + } + + [Fact] + public async Task OneSecondInterval() + { + var adapter = new TestAdapter(TestAdapter.CreateConversation("One_Second_Interval")) + .Use(new ShowTypingMiddleware(100, 1000)); + + await new TestFlow(adapter, async (context, cancellationToken) => + { + await Task.Delay(TimeSpan.FromMilliseconds(2800), cancellationToken); + + // note the ShowTypingMiddleware should not cause the Responded flag to be set + Assert.False(context.Responded); + + await context.SendActivityAsync("Message sent after delay", cancellationToken: cancellationToken); + await Task.CompletedTask; + }) + .Send("foo") + .AssertReply(ValidateTypingActivity, "check typing activity") + .AssertReply(ValidateTypingActivity, "check typing activity") + .AssertReply(ValidateTypingActivity, "check typing activity") + .AssertReply("Message sent after delay") + .StartTestAsync(); + } + + [Fact] + public async Task ContextCompletesBeforeTypingInterval() + { + var adapter = new TestAdapter(TestAdapter.CreateConversation("Context_Completes_Before_Typing_Interval")) + .Use(new ShowTypingMiddleware(100, 5000)); + + await new TestFlow(adapter, async (context, cancellationToken) => + { + await Task.Delay(TimeSpan.FromMilliseconds(2000), cancellationToken); + await context.SendActivityAsync("Message sent after delay", cancellationToken: cancellationToken); + await Task.CompletedTask; + }) + .Send("foo") + .AssertReply(ValidateTypingActivity, "check typing activity") + .AssertReply("Message sent after delay") + .StartTestAsync(); + } + + [Fact] + public async Task ImmediateResponseFiveSecondInterval() + { + var adapter = new TestAdapter(TestAdapter.CreateConversation("ImmediateResponse_5SecondInterval")) + .Use(new ShowTypingMiddleware(2000, 5000)); + + await new TestFlow(adapter, async (context, cancellationToken) => + { + await context.SendActivityAsync("Message sent after delay", cancellationToken: cancellationToken); + await Task.CompletedTask; + }) + .Send("foo") + .AssertReply("Message sent after delay") + .StartTestAsync(); + } + + [Fact] + public async Task ImmediateResponseWhenRunningAsSkill() + { + var adapter = new SkillTestAdapter(TestAdapter.CreateConversation("1_Second_Interval")) + .Use(new ShowTypingMiddleware(100, 1000)); + + await new TestFlow(adapter, async (context, cancellationToken) => + { + await Task.Delay(TimeSpan.FromMilliseconds(2800), cancellationToken); + + // note the ShowTypingMiddleware should not cause the Responded flag to be set + Assert.False(context.Responded); + + await context.SendActivityAsync("Message sent after delay", cancellationToken: cancellationToken); + await Task.CompletedTask; + }) + .Send("foo") + .AssertReply("Message sent after delay") + .StartTestAsync(); + } + + [Fact] + public void ZeroFrequency() + { + try + { + _ = new TestAdapter(TestAdapter.CreateConversation("ZeroFrequency")) + .Use(new ShowTypingMiddleware(-100, 0)); + } + catch (Exception ex) + { + Assert.IsType(ex); + } + } + + private void ValidateTypingActivity(IActivity obj) + { + var activity = obj.AsTypingActivity(); + if (activity != null) + { + return; + } + + throw new Exception("Activity was not of type TypingActivity"); + } + + /// + /// A helper TestAdapter that injects skill claims in the turn so we can test skill use cases. + /// + private class SkillTestAdapter : TestAdapter + { + // An App ID for a parent bot. + private static readonly string _parentBotId = Guid.NewGuid().ToString(); + + // An App ID for a skill bot. + private static readonly string _skillBotId = Guid.NewGuid().ToString(); + + public SkillTestAdapter(ConversationReference conversation = null) + : base(conversation) + { + } + + protected override TurnContext CreateTurnContext(Activity activity) + { + // Get the default turnContext from the base. + var turnContext = base.CreateTurnContext(activity); + + // Create a skill ClaimsIdentity and put it in TurnState so SkillValidation.IsSkillClaim() returns true. + var claimsIdentity = new ClaimsIdentity(); + claimsIdentity.AddClaim(new Claim(AuthenticationConstants.VersionClaim, "2.0")); + claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AudienceClaim, _parentBotId)); + claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AuthorizedParty, _skillBotId)); + turnContext.TurnState.Add(BotIdentityKey, claimsIdentity); + + return turnContext; + } + } + } +} diff --git a/tests/Microsoft.Bot.Builder.Tests/ShowTyping_MiddlewareTests.cs b/tests/Microsoft.Bot.Builder.Tests/ShowTyping_MiddlewareTests.cs deleted file mode 100644 index 7d3f404863..0000000000 --- a/tests/Microsoft.Bot.Builder.Tests/ShowTyping_MiddlewareTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Bot.Schema; -using Xunit; - -namespace Microsoft.Bot.Builder.Tests -{ - public class ShowTyping_MiddlewareTests - { - /// - /// Enum to handle different test cases. - /// - public enum FlowTestCase - { - /// - /// RunAsync is executing on a root bot with no skills (typical standalone bot). - /// - RootBot, - - /// - /// RunAsync is executing in a skill. - /// - Skill - } - - [Fact] - public async Task ShowTyping_TestMiddleware_1_Second_Interval() - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_1_Second_Interval")) - .Use(new MockBotIdentityMiddleware(FlowTestCase.RootBot)) - .Use(new ShowTypingMiddleware(100, 1000)); - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await Task.Delay(TimeSpan.FromMilliseconds(2800)); - - // note the ShowTypingMiddleware should not cause the Responded flag to be set - Assert.False(context.Responded); - - await context.SendActivityAsync("Message sent after delay"); - await Task.CompletedTask; - }) - .Send("foo") - .AssertReply(ValidateTypingActivity, "check typing activity") - .AssertReply(ValidateTypingActivity, "check typing activity") - .AssertReply(ValidateTypingActivity, "check typing activity") - .AssertReply("Message sent after delay") - .StartTestAsync(); - } - - [Fact] - public async Task ShowTyping_TestMiddleware_Context_Completes_Before_Typing_Interval() - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_Context_Completes_Before_Typing_Interval")) - .Use(new MockBotIdentityMiddleware(FlowTestCase.RootBot)) - .Use(new ShowTypingMiddleware(100, 5000)); - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await Task.Delay(TimeSpan.FromMilliseconds(2000)); - await context.SendActivityAsync("Message sent after delay"); - await Task.CompletedTask; - }) - .Send("foo") - .AssertReply(ValidateTypingActivity, "check typing activity") - .AssertReply("Message sent after delay") - .StartTestAsync(); - } - - [Fact] - public async Task ShowTyping_TestMiddleware_ImmediateResponse_5SecondInterval() - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_ImmediateResponse_5SecondInterval")) - .Use(new MockBotIdentityMiddleware(FlowTestCase.RootBot)) - .Use(new ShowTypingMiddleware(2000, 5000)); - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await context.SendActivityAsync("Message sent after delay"); - await Task.CompletedTask; - }) - .Send("foo") - .AssertReply("Message sent after delay") - .StartTestAsync(); - } - - [Fact] - public async Task ShowTyping_TestMiddleware_ImmediateResponse_When_Running_As_Skill() - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_1_Second_Interval")) - .Use(new MockBotIdentityMiddleware(FlowTestCase.Skill)) - .Use(new ShowTypingMiddleware(100, 1000)); - - await new TestFlow(adapter, async (context, cancellationToken) => - { - await Task.Delay(TimeSpan.FromMilliseconds(2800)); - - // note the ShowTypingMiddleware should not cause the Responded flag to be set - Assert.False(context.Responded); - - await context.SendActivityAsync("Message sent after delay"); - await Task.CompletedTask; - }) - .Send("foo") - .AssertReply("Message sent after delay") - .StartTestAsync(); - } - - [Fact] - public void ShowTyping_TestMiddleware_NegativeDelay() - { - try - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_NegativeDelay")) - .Use(new MockBotIdentityMiddleware(FlowTestCase.RootBot)) - .Use(new ShowTypingMiddleware(-100, 1000)); - } - catch (Exception ex) - { - Assert.IsType(ex); - } - } - - [Fact] - public void ShowTyping_TestMiddleware_ZeroFrequency() - { - try - { - TestAdapter adapter = new TestAdapter(TestAdapter.CreateConversation("ShowTyping_TestMiddleware_ZeroFrequency")) - .Use(new ShowTypingMiddleware(-100, 0)); - } - catch (Exception ex) - { - Assert.IsType(ex); - } - } - - private void ValidateTypingActivity(IActivity obj) - { - var activity = obj.AsTypingActivity(); - if (activity != null) - { - return; - } - else - { - throw new Exception("Activity was not of type TypingActivity"); - } - } - - private class MockBotIdentityMiddleware : IMiddleware - { - private readonly FlowTestCase _flowTestCase; - - // An App ID for a parent bot. - private readonly string _parentBotId = Guid.NewGuid().ToString(); - - // An App ID for a skill bot. - private readonly string _skillBotId = Guid.NewGuid().ToString(); - - public MockBotIdentityMiddleware(FlowTestCase flowTestCase) - { - _flowTestCase = flowTestCase; - } - - public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default) - { - // Create a skill ClaimsIdentity and put it in TurnState so SkillValidation.IsSkillClaim() returns true. - var claimsIdentity = new ClaimsIdentity(); - claimsIdentity.AddClaim(new Claim(AuthenticationConstants.VersionClaim, "2.0")); - claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AudienceClaim, _parentBotId)); - - if (_flowTestCase == FlowTestCase.Skill) - { - claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AuthorizedParty, _skillBotId)); - } - - turnContext.TurnState.Add(BotAdapter.BotIdentityKey, claimsIdentity); - - await next(cancellationToken).ConfigureAwait(false); - } - } - } -}