// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.IO; using System.Net; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Teams.Core.Hosting; using Microsoft.Teams.Core.Schema; using Moq; using Moq.Protected; namespace Microsoft.Teams.Core.UnitTests; public class BotApplicationTests { [Fact] public void Constructor_InitializesProperties() { ConversationClient conversationClient = CreateMockConversationClient(); UserTokenClient userTokenClient = CreateMockUserTokenClient(); NullLogger logger = NullLogger.Instance; BotApplication botApp = new(conversationClient, userTokenClient, logger, CreateOptions("test-app-id")); Assert.NotNull(botApp); Assert.NotNull(botApp.ConversationClient); Assert.NotNull(botApp.UserTokenClient); Assert.NotNull(botApp.UserTokenClient); } [Fact] public async Task ProcessAsync_WithNullHttpContext_ThrowsArgumentNullException() { BotApplication botApp = CreateBotApplication(); await Assert.ThrowsAsync(() => botApp.ProcessAsync(null!)); } [Fact] public async Task ProcessAsync_WithValidActivity_ProcessesSuccessfully() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123" }; activity.Properties["text"] = "Test message"; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); bool onActivityCalled = false; botApp.OnActivity = (act, ct) => { onActivityCalled = true; return Task.CompletedTask; }; await botApp.ProcessAsync(httpContext); Assert.True(onActivityCalled); } [Fact] public async Task ProcessAsync_WithMiddleware_ExecutesMiddleware() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123" }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); bool middlewareCalled = false; Mock mockMiddleware = new(); mockMiddleware .Setup(m => m.OnTurnAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback(async (app, act, next, ct) => { middlewareCalled = true; await next(ct); }) .Returns(Task.CompletedTask); botApp.UseMiddleware(mockMiddleware.Object); bool onActivityCalled = false; botApp.OnActivity = (act, ct) => { onActivityCalled = true; return Task.CompletedTask; }; await botApp.ProcessAsync(httpContext); Assert.True(middlewareCalled); Assert.True(onActivityCalled); } [Fact] public async Task ProcessAsync_WithException_ThrowsBotHandlerException() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123" }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); botApp.OnActivity = (act, ct) => throw new InvalidOperationException("Test exception"); BotHandlerException exception = await Assert.ThrowsAsync(() => botApp.ProcessAsync(httpContext)); Assert.Equal("Error processing activity", exception.Message); Assert.IsType(exception.InnerException); } [Fact] public void Use_AddsMiddlewareToChain() { BotApplication botApp = CreateBotApplication(); Mock mockMiddleware = new(); ITurnMiddleware result = botApp.UseMiddleware(mockMiddleware.Object); Assert.NotNull(result); } [Fact] public async Task SendActivityAsync_WithValidActivity_SendsSuccessfully() { Mock mockHttpMessageHandler = new(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":\"activity123\"}") }); HttpClient httpClient = new(mockHttpMessageHandler.Object); ConversationClient conversationClient = new(httpClient); UserTokenClient userTokenClient = CreateMockUserTokenClient(); NullLogger logger = NullLogger.Instance; BotApplication botApp = new(conversationClient, userTokenClient, logger); CoreActivity activity = new() { Type = ActivityType.Message, ServiceUrl = new Uri("https://test.service.url/"), Conversation = new("conv123") }; SendActivityResponse? result = await botApp.SendActivityAsync(activity); Assert.NotNull(result); Assert.Contains("activity123", result.Id); } [Fact] public async Task SendActivityAsync_WithNullActivity_ThrowsArgumentNullException() { BotApplication botApp = CreateBotApplication(); await Assert.ThrowsAsync(() => botApp.SendActivityAsync(null!)); } [Fact] public async Task ProcessAsync_ServiceUrlClaimMatchesActivity_ProcessesSuccessfully() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123", ServiceUrl = new Uri("https://smba.trafficmanager.net/teams/") }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); httpContext.User = new ClaimsPrincipal(new ClaimsIdentity( [ new Claim("serviceurl", "https://smba.trafficmanager.net/teams/") ])); bool onActivityCalled = false; botApp.OnActivity = (act, ct) => { onActivityCalled = true; return Task.CompletedTask; }; await botApp.ProcessAsync(httpContext); Assert.True(onActivityCalled); } [Fact] public async Task ProcessAsync_ServiceUrlClaimMismatch_ThrowsInvalidDataException() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123", ServiceUrl = new Uri("https://smba.trafficmanager.net/teams/") }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); httpContext.User = new ClaimsPrincipal(new ClaimsIdentity( [ new Claim("serviceurl", "https://evil.example.com/") ])); botApp.OnActivity = (act, ct) => Task.CompletedTask; InvalidDataException exception = await Assert.ThrowsAsync(() => botApp.ProcessAsync(httpContext)); Assert.Contains("does not match", exception.Message); } [Fact] public async Task ProcessAsync_ServiceUrlClaimMismatchCase_ThrowsInvalidDataException() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123", ServiceUrl = new Uri("https://smba.trafficmanager.net/teams/") }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); httpContext.User = new ClaimsPrincipal(new ClaimsIdentity( [ new Claim("serviceurl", "https://SMBA.trafficmanager.net/teams/") ])); botApp.OnActivity = (act, ct) => Task.CompletedTask; InvalidDataException exception = await Assert.ThrowsAsync(() => botApp.ProcessAsync(httpContext)); Assert.Contains("does not match", exception.Message); } [Fact] public async Task ProcessAsync_NoServiceUrlClaim_ProcessesSuccessfully() { BotApplication botApp = CreateBotApplication(); CoreActivity activity = new() { Type = ActivityType.Message, Id = "act123", ServiceUrl = new Uri("https://smba.trafficmanager.net/teams/") }; DefaultHttpContext httpContext = CreateHttpContextWithActivity(activity); // No serviceurl claim set — default ClaimsPrincipal has no claims bool onActivityCalled = false; botApp.OnActivity = (act, ct) => { onActivityCalled = true; return Task.CompletedTask; }; await botApp.ProcessAsync(httpContext); Assert.True(onActivityCalled); } private static BotApplicationOptions CreateOptions(string appId) => new() { AppId = appId }; private static BotApplication CreateBotApplication() => new(CreateMockConversationClient(), CreateMockUserTokenClient(), NullLogger.Instance); private static ConversationClient CreateMockConversationClient() { Mock mockHttpClient = new(); return new ConversationClient(mockHttpClient.Object); } private static UserTokenClient CreateMockUserTokenClient() { Mock mockHttpClient = new(); NullLogger logger = NullLogger.Instance; Mock mockConfiguration = new(); return new UserTokenClient(mockHttpClient.Object, mockConfiguration.Object, logger); } private static DefaultHttpContext CreateHttpContextWithActivity(CoreActivity activity) { DefaultHttpContext httpContext = new(); string activityJson = activity.ToJson(); byte[] bodyBytes = Encoding.UTF8.GetBytes(activityJson); httpContext.Request.Body = new MemoryStream(bodyBytes); httpContext.Request.ContentType = "application/json"; return httpContext; } }