diff --git a/core/.claude/settings.local.json b/core/.claude/settings.local.json new file mode 100644 index 00000000..279b23c3 --- /dev/null +++ b/core/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)", + "Bash(dotnet test:*)", + "Bash(dotnet clean:*)" + ] + } +} diff --git a/core/samples/CompatBot/EchoBot.cs b/core/samples/CompatBot/EchoBot.cs index 6b8da45e..e349e430 100644 --- a/core/samples/CompatBot/EchoBot.cs +++ b/core/samples/CompatBot/EchoBot.cs @@ -149,7 +149,7 @@ await conversationClient.UpdateActivityAsync( await Task.Delay(2000, cancellationToken); - await conversationClient.DeleteActivityAsync(cr.Conversation.Id, res.Id!, new Uri(turnContext.Activity.ServiceUrl), AgenticIdentity.FromProperties(ca.From.Properties), null, cancellationToken); + await conversationClient.DeleteActivityAsync(cr.Conversation.Id, res.Id!, new Uri(turnContext.Activity.ServiceUrl), AgenticIdentity.FromProperties(ca.From?.Properties), null, cancellationToken); await turnContext.SendActivityAsync(MessageFactory.Text("Proactive message sent and deleted."), cancellationToken); } diff --git a/core/samples/CompatBot/MyCompatMiddleware.cs b/core/samples/CompatBot/MyCompatMiddleware.cs index 084384b6..ef87a97e 100644 --- a/core/samples/CompatBot/MyCompatMiddleware.cs +++ b/core/samples/CompatBot/MyCompatMiddleware.cs @@ -7,11 +7,14 @@ namespace CompatBot { public class MyCompatMiddleware : Microsoft.Bot.Builder.IMiddleware { - public Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default) + public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default) { Console.WriteLine("MyCompatMiddleware: OnTurnAsync"); Console.WriteLine(turnContext.Activity.Text); - return next(cancellationToken); + + await turnContext.SendActivityAsync(MessageFactory.Text("Hello from MyCompatMiddleware!"), cancellationToken); + + await next(cancellationToken).ConfigureAwait(false); } } } diff --git a/core/samples/CompatBot/Program.cs b/core/samples/CompatBot/Program.cs index 81f04a1d..ee8614ee 100644 --- a/core/samples/CompatBot/Program.cs +++ b/core/samples/CompatBot/Program.cs @@ -32,6 +32,7 @@ CompatAdapter compatAdapter = (CompatAdapter)app.Services.GetRequiredService(); compatAdapter.Use(new MyCompatMiddleware()); +compatAdapter.Use(new MyCompatMiddleware()); app.MapPost("/api/messages", async (IBotFrameworkHttpAdapter adapter, IBot bot, HttpRequest request, HttpResponse response, CancellationToken ct) => await adapter.ProcessAsync(request, response, bot, ct)); diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/Entity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/Entity.cs index b0186cb3..dc3d068e 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/Entity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/Entity.cs @@ -45,9 +45,9 @@ public class EntityList : List /// public static EntityList FromJsonArray(JsonArray? jsonArray, JsonSerializerOptions? options = null) { - if (jsonArray == null) + if (jsonArray is null) { - return []; + return null!; } EntityList entities = []; foreach (JsonNode? item in jsonArray) diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs index e2680bd7..7851dfee 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs @@ -87,9 +87,18 @@ protected TeamsActivity(CoreActivity activity) : base(activity) { ChannelData = new TeamsChannelData(activity.ChannelData); } - From = new TeamsConversationAccount(activity.From); - Recipient = new TeamsConversationAccount(activity.Recipient); + + if (activity.From is not null) + { + From = new TeamsConversationAccount(activity.From); + } + + if (activity.Recipient is not null) + { + Recipient = new TeamsConversationAccount(activity.Recipient); + } Conversation = new TeamsConversation(activity.Conversation); + Attachments = TeamsAttachment.FromJArray(activity.Attachments); Entities = EntityList.FromJsonArray(activity.Entities); @@ -104,7 +113,10 @@ internal TeamsActivity Rebase() { base.Attachments = this.Attachments?.ToJsonArray(); base.Entities = this.Entities?.ToJsonArray(); - base.ChannelData = new TeamsChannelData(this.ChannelData); + if (this.ChannelData is not null) + { + base.ChannelData = new TeamsChannelData(this.ChannelData); + } base.From = this.From; base.Recipient = this.Recipient; base.Conversation = this.Conversation; @@ -116,12 +128,12 @@ internal TeamsActivity Rebase() /// /// Gets or sets the account information for the sender of the Teams conversation. /// - [JsonPropertyName("from")] public new TeamsConversationAccount From { get; set; } + [JsonPropertyName("from")] public new TeamsConversationAccount? From { get; set; } /// /// Gets or sets the account information for the recipient of the Teams conversation. /// - [JsonPropertyName("recipient")] public new TeamsConversationAccount Recipient { get; set; } + [JsonPropertyName("recipient")] public new TeamsConversationAccount? Recipient { get; set; } /// /// Gets or sets the conversation information for the Teams conversation. diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsAttachment.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsAttachment.cs index 1746378c..21a7b22f 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsAttachment.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsAttachment.cs @@ -35,7 +35,7 @@ static internal IList FromJArray(JsonArray? jsonArray) { if (jsonArray is null) { - return []; + return null!; } List attachments = []; foreach (JsonNode? item in jsonArray) diff --git a/core/src/Microsoft.Teams.Bot.Compat/CompatAdapter.cs b/core/src/Microsoft.Teams.Bot.Compat/CompatAdapter.cs index 81142ecb..4182b9ba 100644 --- a/core/src/Microsoft.Teams.Bot.Compat/CompatAdapter.cs +++ b/core/src/Microsoft.Teams.Bot.Compat/CompatAdapter.cs @@ -5,6 +5,7 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Schema; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Teams.Bot.Apps; using Microsoft.Teams.Bot.Core; using Microsoft.Teams.Bot.Core.Schema; @@ -20,10 +21,24 @@ namespace Microsoft.Teams.Bot.Compat; /// The adapter allows registration of middleware and error handling delegates, and supports processing HTTP requests /// and continuing conversations. Thread safety is not guaranteed; instances should not be shared across concurrent /// requests. -/// The bot application instance that handles activity processing and manages user token operations. -/// The underlying bot adapter used to interact with the bot framework and create turn contexts. -public class CompatAdapter(TeamsBotApplication botApplication, CompatBotAdapter compatBotAdapter) : IBotFrameworkHttpAdapter +public class CompatAdapter : IBotFrameworkHttpAdapter { + private readonly TeamsBotApplication _teamsBotApplication; + private readonly CompatBotAdapter _compatBotAdapter; + private readonly IServiceProvider _sp; + + + /// + /// Creates a new instance of the class. + /// + /// + public CompatAdapter(IServiceProvider sp) + { + _sp = sp; + _teamsBotApplication = sp.GetRequiredService(); + _compatBotAdapter = sp.GetRequiredService(); + } + /// /// Gets the collection of middleware components configured for the application. /// @@ -64,26 +79,22 @@ public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpRespons ArgumentNullException.ThrowIfNull(httpRequest); ArgumentNullException.ThrowIfNull(httpResponse); ArgumentNullException.ThrowIfNull(bot); + CoreActivity? coreActivity = null; - botApplication.OnActivity = async (activity, cancellationToken1) => + _teamsBotApplication.OnActivity = async (activity, cancellationToken1) => { coreActivity = activity; - TurnContext turnContext = new(compatBotAdapter, activity.ToCompatActivity()); - turnContext.TurnState.Add(new CompatUserTokenClient(botApplication.UserTokenClient)); - CompatConnectorClient connectionClient = new(new CompatConversations(botApplication.ConversationClient) { ServiceUrl = activity.ServiceUrl?.ToString() }); + TurnContext turnContext = new(_compatBotAdapter, activity.ToCompatActivity()); + turnContext.TurnState.Add(new CompatUserTokenClient(_teamsBotApplication.UserTokenClient)); + CompatConnectorClient connectionClient = new(new CompatConversations(_teamsBotApplication.ConversationClient) { ServiceUrl = activity.ServiceUrl?.ToString() }); turnContext.TurnState.Add(connectionClient); - turnContext.TurnState.Add(botApplication.TeamsApiClient); - await bot.OnTurnAsync(turnContext, cancellationToken1).ConfigureAwait(false); + turnContext.TurnState.Add(_teamsBotApplication.TeamsApiClient); + await MiddlewareSet.ReceiveActivityWithStatusAsync(turnContext, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); }; try { - foreach (Microsoft.Bot.Builder.IMiddleware? middleware in MiddlewareSet) - { - botApplication.Use(new CompatAdapterMiddleware(middleware)); - } - - await botApplication.ProcessAsync(httpRequest.HttpContext, cancellationToken).ConfigureAwait(false); + await _teamsBotApplication.ProcessAsync(httpRequest.HttpContext, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -92,7 +103,7 @@ public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpRespons if (ex is BotHandlerException aex) { coreActivity = aex.Activity; - using TurnContext turnContext = new(compatBotAdapter, coreActivity!.ToCompatActivity()); + using TurnContext turnContext = new(_compatBotAdapter, coreActivity!.ToCompatActivity()); await OnTurnError(turnContext, ex).ConfigureAwait(false); } else @@ -124,9 +135,9 @@ public async Task ContinueConversationAsync(string botId, ConversationReference ArgumentNullException.ThrowIfNull(reference); ArgumentNullException.ThrowIfNull(callback); - using TurnContext turnContext = new(compatBotAdapter, reference.GetContinuationActivity()); - turnContext.TurnState.Add(new CompatConnectorClient(new CompatConversations(botApplication.ConversationClient) { ServiceUrl = reference.ServiceUrl })); - turnContext.TurnState.Add(botApplication.TeamsApiClient); + using TurnContext turnContext = new(_compatBotAdapter, reference.GetContinuationActivity()); + turnContext.TurnState.Add(new CompatConnectorClient(new CompatConversations(_teamsBotApplication.ConversationClient) { ServiceUrl = reference.ServiceUrl })); + turnContext.TurnState.Add(_teamsBotApplication.TeamsApiClient); await callback(turnContext, cancellationToken).ConfigureAwait(false); } } diff --git a/core/src/Microsoft.Teams.Bot.Compat/CompatAdapterMiddleware.cs b/core/src/Microsoft.Teams.Bot.Compat/CompatAdapterMiddleware.cs deleted file mode 100644 index 4cdf9f7a..00000000 --- a/core/src/Microsoft.Teams.Bot.Compat/CompatAdapterMiddleware.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Teams.Bot.Apps; -using Microsoft.Teams.Bot.Core; -using Microsoft.Teams.Bot.Core.Schema; - -namespace Microsoft.Teams.Bot.Compat; - -/// -/// Adapts Bot Framework SDK middleware to work with the Teams Bot Core middleware pipeline. -/// -/// -/// This adapter enables legacy Bot Framework middleware components to be used in the new Teams Bot Core architecture. -/// It converts CoreActivity instances to Bot Framework Activity format, creates appropriate turn contexts with -/// compatibility clients (UserTokenClient and ConnectorClient), and delegates processing to the Bot Framework middleware. -/// This allows gradual migration from Bot Framework SDK to Teams Bot Core while preserving existing middleware investments. -/// -/// The Bot Framework middleware component to adapt into the Teams Bot Core pipeline. -internal sealed class CompatAdapterMiddleware(IMiddleware bfMiddleWare) : ITurnMiddleWare -{ - /// - /// Processes a turn by converting the CoreActivity to Bot Framework format and invoking the wrapped middleware. - /// - /// The bot application processing the turn. Must be a TeamsBotApplication instance. - /// The activity to process in Core format. - /// A delegate to invoke the next middleware in the pipeline. - /// A cancellation token that can be used to cancel the asynchronous operation. - /// A task that represents the asynchronous operation. - public Task OnTurnAsync(BotApplication botApplication, CoreActivity activity, NextTurn nextTurn, CancellationToken cancellationToken = default) - { - - if (botApplication is TeamsBotApplication tba) - { -#pragma warning disable CA2000 // Dispose objects before losing scope - TurnContext turnContext = new(new CompatBotAdapter(tba), activity.ToCompatActivity()); -#pragma warning restore CA2000 // Dispose objects before losing scope - - turnContext.TurnState.Add( - new CompatUserTokenClient(botApplication.UserTokenClient) - ); - - turnContext.TurnState.Add( - new CompatConnectorClient( - new CompatConversations(botApplication.ConversationClient) - { - ServiceUrl = activity.ServiceUrl?.ToString() - } - ) - ); - - turnContext.TurnState.Add(tba.TeamsApiClient); - - return bfMiddleWare.OnTurnAsync(turnContext, (activity) - => nextTurn(cancellationToken), cancellationToken); - } - return Task.CompletedTask; - } - -} diff --git a/core/src/Microsoft.Teams.Bot.Compat/CompatBotAdapter.cs b/core/src/Microsoft.Teams.Bot.Compat/CompatBotAdapter.cs index 706bf423..f611918e 100644 --- a/core/src/Microsoft.Teams.Bot.Compat/CompatBotAdapter.cs +++ b/core/src/Microsoft.Teams.Bot.Compat/CompatBotAdapter.cs @@ -112,8 +112,11 @@ private void WriteInvokeResponseToHttpResponse(InvokeResponse? invokeResponse) response.StatusCode = invokeResponse.Status; using StreamWriter httpResponseStreamWriter = new(response.BodyWriter.AsStream()); using JsonTextWriter httpResponseJsonWriter = new(httpResponseStreamWriter); - logger.LogTrace("Sending Invoke Response: \n {InvokeResponse} \n", System.Text.Json.JsonSerializer.Serialize(invokeResponse.Body, _writeIndentedJsonOptions)); - Microsoft.Bot.Builder.Integration.AspNet.Core.HttpHelper.BotMessageSerializer.Serialize(httpResponseJsonWriter, invokeResponse.Body); + logger.LogTrace("Sending Invoke Response: \n {InvokeResponse} with status: {Status} \n", System.Text.Json.JsonSerializer.Serialize(invokeResponse.Body, _writeIndentedJsonOptions), invokeResponse.Status); + if (invokeResponse.Body is not null) + { + Microsoft.Bot.Builder.Integration.AspNet.Core.HttpHelper.BotMessageSerializer.Serialize(httpResponseJsonWriter, invokeResponse.Body); + } } else { diff --git a/core/src/Microsoft.Teams.Bot.Compat/CompatConversations.cs b/core/src/Microsoft.Teams.Bot.Compat/CompatConversations.cs index 75b161ec..62e12751 100644 --- a/core/src/Microsoft.Teams.Bot.Compat/CompatConversations.cs +++ b/core/src/Microsoft.Teams.Bot.Compat/CompatConversations.cs @@ -58,7 +58,7 @@ public async Task> CreateCon CreateConversationResponse res = await _client.CreateConversationAsync( convoParams, new Uri(ServiceUrl), - AgenticIdentity.FromProperties(convoParams.Activity?.From.Properties), + AgenticIdentity.FromProperties(convoParams.Activity?.From?.Properties), convertedHeaders, cancellationToken).ConfigureAwait(false); diff --git a/core/src/Microsoft.Teams.Bot.Compat/CompatTeamsInfo.cs b/core/src/Microsoft.Teams.Bot.Compat/CompatTeamsInfo.cs index e8711045..3320dc12 100644 --- a/core/src/Microsoft.Teams.Bot.Compat/CompatTeamsInfo.cs +++ b/core/src/Microsoft.Teams.Bot.Compat/CompatTeamsInfo.cs @@ -54,7 +54,7 @@ private static string GetServiceUrl(ITurnContext turnContext) private static AgenticIdentity GetIdentity(ITurnContext turnContext) { var coreActivity = ((Activity)turnContext.Activity).FromCompatActivity(); - return AgenticIdentity.FromProperties(coreActivity.From.Properties) ?? new AgenticIdentity(); + return AgenticIdentity.FromProperties(coreActivity.From?.Properties) ?? new AgenticIdentity(); } #endregion diff --git a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs index bf89a24e..0aa088c0 100644 --- a/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs @@ -51,6 +51,7 @@ public async Task SendActivityAsync(CoreActivity activity, string convId = conversationId.Length > 325 ? conversationId[..325] : conversationId; url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{convId}/activities"; } + activity.ServiceUrl = null; // do not serialize in the outgoing payload string body = activity.ToJson(); @@ -60,7 +61,7 @@ public async Task SendActivityAsync(CoreActivity activity, HttpMethod.Post, url, body, - CreateRequestOptions(activity.From.GetAgenticIdentity(), "sending activity", customHeaders), + CreateRequestOptions(activity.From?.GetAgenticIdentity(), "sending activity", customHeaders), cancellationToken).ConfigureAwait(false))!; } @@ -90,7 +91,7 @@ public async Task UpdateActivityAsync(string conversatio HttpMethod.Put, url, body, - CreateRequestOptions(activity.From.GetAgenticIdentity(), "updating activity", customHeaders), + CreateRequestOptions(activity.From?.GetAgenticIdentity(), "updating activity", customHeaders), cancellationToken).ConfigureAwait(false))!; } @@ -144,7 +145,7 @@ await DeleteActivityAsync( activity.Conversation.Id, activity.Id, activity.ServiceUrl, - activity.From.GetAgenticIdentity(), + activity.From?.GetAgenticIdentity(), customHeaders, cancellationToken).ConfigureAwait(false); } diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs index 3fc0846b..4c88e717 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs @@ -52,11 +52,11 @@ public class CoreActivity /// /// Gets or sets the account that sent this activity. /// - [JsonPropertyName("from")] public ConversationAccount From { get; set; } = new(); + [JsonPropertyName("from")] public ConversationAccount? From { get; set; } /// /// Gets or sets the account that should receive this activity. /// - [JsonPropertyName("recipient")] public ConversationAccount Recipient { get; set; } = new(); + [JsonPropertyName("recipient")] public ConversationAccount? Recipient { get; set; } /// /// Gets or sets the conversation in which this activity is taking place. /// diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs index d05e635e..00dbdb8a 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs @@ -37,17 +37,13 @@ protected CoreActivityBuilder(TActivity activity) public TBuilder WithConversationReference(TActivity activity) { ArgumentNullException.ThrowIfNull(activity); - ArgumentNullException.ThrowIfNull(activity.ChannelId); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); ArgumentNullException.ThrowIfNull(activity.Conversation); - ArgumentNullException.ThrowIfNull(activity.From); ArgumentNullException.ThrowIfNull(activity.Recipient); - + WithServiceUrl(activity.ServiceUrl); - WithChannelId(activity.ChannelId); SetConversation(activity.Conversation); SetFrom(activity.Recipient); - SetRecipient(activity.From); return (TBuilder)this; } @@ -128,9 +124,12 @@ public TBuilder WithProperty(string name, T? value) /// /// The sender account. /// The builder instance for chaining. - public TBuilder WithFrom(ConversationAccount from) + public TBuilder WithFrom(ConversationAccount? from) { - SetFrom(from); + if (from is not null) + { + SetFrom(from); + } return (TBuilder)this; } @@ -163,7 +162,10 @@ public TBuilder WithConversation(Conversation conversation) /// The builder instance for chaining. public virtual TBuilder WithChannelData(ChannelData? channelData) { - _activity.ChannelData = channelData; + if (channelData is not null) + { + _activity.ChannelData = channelData; + } return (TBuilder)this; } diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs index 1964ef5c..a88ecf52 100644 --- a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs @@ -21,8 +21,6 @@ public void Constructor_DefaultConstructor_CreatesNewActivity() TeamsActivity activity = builder.Build(); Assert.NotNull(activity); - Assert.NotNull(activity.From); - Assert.NotNull(activity.Recipient); Assert.NotNull(activity.Conversation); } @@ -113,6 +111,7 @@ public void WithFrom_SetsSenderAccount() .WithFrom(fromAccount) .Build(); + Assert.NotNull(activity.From); Assert.Equal("sender-id", activity.From.Id); Assert.Equal("Sender Name", activity.From.Name); } @@ -130,6 +129,7 @@ public void WithRecipient_SetsRecipientAccount() .WithRecipient(recipientAccount) .Build(); + Assert.NotNull(activity.Recipient); Assert.Equal("recipient-id", activity.Recipient.Id); Assert.Equal("Recipient Name", activity.Recipient.Name); } @@ -448,7 +448,9 @@ public void FluentAPI_CompleteActivity_BuildsCorrectly() Assert.Equal("activity-123", activity.Id); Assert.Equal("msteams", activity.ChannelId); Assert.Equal("User Test message", activity.Properties["text"]); + Assert.NotNull(activity.From); Assert.Equal("sender-id", activity.From.Id); + Assert.NotNull(activity.Recipient); Assert.Equal("recipient-id", activity.Recipient.Id); Assert.Equal("conv-id", activity.Conversation.Id); Assert.NotNull(activity.Entities); @@ -522,20 +524,18 @@ public void AddMention_UpdatesBaseEntityCollection() [Fact] public void WithChannelData_NullValue_SetsToNull() { - TeamsActivity activity = builder - .WithChannelData(null!) - .Build(); + TeamsChannelData channelData = new() { TeamsChannelId = "channel-1" }; + TeamsActivityBuilder testBuilder = TeamsActivity.CreateBuilder(); + testBuilder.WithChannelData(channelData); + testBuilder.WithChannelData(null!); + TeamsActivity activity = testBuilder.Build(); Assert.Null(activity.ChannelData); } [Fact] - public void AddEntity_NullEntitiesCollection_InitializesCollection() + public void AddEntity_InitializesCollectionIfNeeded() { - TeamsActivity activity = builder.Build(); - - Assert.NotNull(activity.Entities); - ClientInfoEntity entity = new() { Locale = "en-US" }; builder.AddEntity(entity); @@ -545,12 +545,8 @@ public void AddEntity_NullEntitiesCollection_InitializesCollection() } [Fact] - public void AddAttachment_NullAttachmentsCollection_InitializesCollection() + public void AddAttachment_InitializesCollectionIfNeeded() { - TeamsActivity activity = builder.Build(); - - Assert.NotNull(activity.Attachments); - TeamsAttachment attachment = new() { ContentType = "text/html" }; builder.AddAttachment(attachment); @@ -582,19 +578,20 @@ public void WithConversationReference_WithNullActivity_ThrowsArgumentNullExcepti } [Fact] - public void WithConversationReference_WithNullChannelId_ThrowsArgumentNullException() + public void WithConversationReference_WithNullChannelId_DoesNotThrow() { - TeamsActivity sourceActivity = new() - { - ChannelId = null, - ServiceUrl = new Uri("https://test.com"), - Conversation = new TeamsConversation(new Conversation()), - From = new TeamsConversationAccount(new ConversationAccount()), - Recipient = new TeamsConversationAccount(new ConversationAccount()) - }; + TeamsActivity sourceActivity = TeamsActivity.CreateBuilder() + .WithServiceUrl(new Uri("https://test.com")) + .WithConversation(new TeamsConversation(new Conversation())) + .WithFrom(new TeamsConversationAccount(new ConversationAccount())) + .WithRecipient(new TeamsConversationAccount(new ConversationAccount { Id = "bot-1" })) + .Build(); - Assert.Throws(() => builder.WithConversationReference(sourceActivity)); + TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); + // ChannelId is not set by WithConversationReference when null + Assert.Equal(ActivityType.Message, result.Type); + Assert.NotNull(result.From); } [Fact] @@ -613,41 +610,42 @@ public void WithConversationReference_WithNullServiceUrl_ThrowsArgumentNullExcep } [Fact] - public void WithConversationReference_WithEmptyConversationId_DoesNotThrow() + public void WithConversationReference_WithEmptyConversationId_SetsFromRecipient() { - TeamsActivity sourceActivity = new() - { - ChannelId = "msteams", - ServiceUrl = new Uri("https://test.com"), - Conversation = new TeamsConversation(new Conversation()), - From = new TeamsConversationAccount(new ConversationAccount { Id = "user-1" }), - Recipient = new TeamsConversationAccount(new ConversationAccount { Id = "bot-1" }) - }; + TeamsActivity sourceActivity = TeamsActivity.CreateBuilder() + .WithChannelId("msteams") + .WithServiceUrl(new Uri("https://test.com")) + .WithConversation(new TeamsConversation(new Conversation())) + .WithFrom(new TeamsConversationAccount(new ConversationAccount { Id = "user-1" })) + .WithRecipient(new TeamsConversationAccount(new ConversationAccount { Id = "bot-1" })) + .Build(); TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); Assert.NotNull(result.Conversation); + Assert.NotNull(result.From); + Assert.Equal("bot-1", result.From.Id); } [Fact] - public void WithConversationReference_WithEmptyFromId_DoesNotThrow() + public void WithConversationReference_WithEmptyFromId_SetsFromRecipient() { - TeamsActivity sourceActivity = new() - { - ChannelId = "msteams", - ServiceUrl = new Uri("https://test.com"), - Conversation = new TeamsConversation(new Conversation { Id = "conv-1" }), - From = new TeamsConversationAccount(new ConversationAccount()), - Recipient = new TeamsConversationAccount(new ConversationAccount { Id = "bot-1" }) - }; + TeamsActivity sourceActivity = TeamsActivity.CreateBuilder() + .WithChannelId("msteams") + .WithServiceUrl(new Uri("https://test.com")) + .WithConversation(new TeamsConversation(new Conversation { Id = "conv-1" })) + .WithFrom(new TeamsConversationAccount(new ConversationAccount())) + .WithRecipient(new TeamsConversationAccount(new ConversationAccount { Id = "bot-1" })) + .Build(); TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); Assert.NotNull(result.From); + Assert.Equal("bot-1", result.From.Id); } [Fact] - public void WithConversationReference_WithEmptyRecipientId_DoesNotThrow() + public void WithConversationReference_WithEmptyRecipientId_ThrowsArgumentNullException() { TeamsActivity sourceActivity = new() { @@ -655,12 +653,10 @@ public void WithConversationReference_WithEmptyRecipientId_DoesNotThrow() ServiceUrl = new Uri("https://test.com"), Conversation = new TeamsConversation(new Conversation { Id = "conv-1" }), From = new TeamsConversationAccount(new ConversationAccount { Id = "user-1" }), - Recipient = new TeamsConversationAccount(new ConversationAccount()) + Recipient = null! }; - TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); - - Assert.NotNull(result.Recipient); + Assert.Throws(() => builder.WithConversationReference(sourceActivity)); } [Fact] @@ -676,6 +672,7 @@ public void WithFrom_WithBaseConversationAccount_ConvertsToTeamsConversationAcco .WithFrom(baseAccount) .Build(); + Assert.NotNull(activity.From); Assert.IsType(activity.From); Assert.Equal("user-123", activity.From.Id); Assert.Equal("User Name", activity.From.Name); @@ -694,6 +691,7 @@ public void WithRecipient_WithBaseConversationAccount_ConvertsToTeamsConversatio .WithRecipient(baseAccount) .Build(); + Assert.NotNull(activity.Recipient); Assert.IsType(activity.Recipient); Assert.Equal("bot-123", activity.Recipient.Id); Assert.Equal("Bot Name", activity.Recipient.Name); @@ -834,7 +832,9 @@ public void IntegrationTest_CreateComplexActivity() Assert.Equal(serviceUrl, activity.ServiceUrl); Assert.Equal("msteams", activity.ChannelId); Assert.Equal("Manager Please review this document", activity.Properties["text"]); + Assert.NotNull(activity.From); Assert.Equal("bot-id", activity.From.Id); + Assert.NotNull(activity.Recipient); Assert.Equal("user-id", activity.Recipient.Id); Assert.Equal("conv-001", activity.Conversation.Id); Assert.Equal("tenant-001", activity.Conversation.TenantId); diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs index 77c9b93b..791f336c 100644 --- a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs @@ -28,7 +28,7 @@ public void DeserializeTeamsActivityWithTeamsChannelData() TeamsChannelData tcd = activity.ChannelData!; Assert.Equal("19:6848757105754c8981c67612732d9aa7@thread.tacv2", tcd.TeamsChannelId); Assert.Equal("19:6848757105754c8981c67612732d9aa7@thread.tacv2", tcd.Channel!.Id); - Assert.Equal("b15a9416-0ad3-4172-9210-7beb711d3f70", activity.From.AadObjectId); + Assert.Equal("b15a9416-0ad3-4172-9210-7beb711d3f70", activity.From!.AadObjectId); Assert.Equal("19:6848757105754c8981c67612732d9aa7@thread.tacv2;messageid=1759881511856", activity.Conversation.Id); Assert.NotNull(activity.Attachments); diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/BotApplicationTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/BotApplicationTests.cs index 9f0dc316..058f4153 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/BotApplicationTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/BotApplicationTests.cs @@ -54,7 +54,8 @@ public async Task ProcessAsync_WithValidActivity_ProcessesSuccessfully() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Properties["text"] = "Test message"; activity.Recipient.Properties["appId"] = "test-app-id"; @@ -86,7 +87,8 @@ public async Task ProcessAsync_WithMiddleware_ExecutesMiddleware() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; @@ -130,7 +132,8 @@ public async Task ProcessAsync_WithException_ThrowsBotHandlerException() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs index c3f62fd0..14a6c79e 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/CoreActivityBuilderTests.cs @@ -14,8 +14,6 @@ public void Constructor_DefaultConstructor_CreatesNewActivity() CoreActivity activity = builder.Build(); Assert.NotNull(activity); - Assert.NotNull(activity.From); - Assert.NotNull(activity.Recipient); Assert.NotNull(activity.Conversation); } @@ -104,6 +102,7 @@ public void WithFrom_SetsSenderAccount() .WithFrom(fromAccount) .Build(); + Assert.NotNull(activity.From); Assert.Equal("sender-id", activity.From.Id); Assert.Equal("Sender Name", activity.From.Name); } @@ -121,6 +120,7 @@ public void WithRecipient_SetsRecipientAccount() .WithRecipient(recipientAccount) .Build(); + Assert.NotNull(activity.Recipient); Assert.Equal("recipient-id", activity.Recipient.Id); Assert.Equal("Recipient Name", activity.Recipient.Name); } @@ -181,7 +181,9 @@ public void FluentAPI_CompleteActivity_BuildsCorrectly() Assert.Equal("activity-123", activity.Id); Assert.Equal("msteams", activity.ChannelId); Assert.Equal("Test message", activity.Properties["text"]?.ToString()); + Assert.NotNull(activity.From); Assert.Equal("sender-id", activity.From.Id); + Assert.NotNull(activity.Recipient); Assert.Equal("recipient-id", activity.Recipient.Id); Assert.Equal("conv-id", activity.Conversation.Id); } @@ -238,7 +240,7 @@ public void WithConversationReference_WithNullActivity_ThrowsArgumentNullExcepti } [Fact] - public void WithConversationReference_WithNullChannelId_ThrowsArgumentNullException() + public void WithConversationReference_WithNullChannelId_DoesNotThrow() { CoreActivityBuilder builder = new(); CoreActivity sourceActivity = new() @@ -250,7 +252,9 @@ public void WithConversationReference_WithNullChannelId_ThrowsArgumentNullExcept Recipient = new ConversationAccount() }; - Assert.Throws(() => builder.WithConversationReference(sourceActivity)); + CoreActivity activity = builder.WithConversationReference(sourceActivity).Build(); + // ChannelId is not set by WithConversationReference when null + Assert.Equal(ActivityType.Message, activity.Type); } [Fact] @@ -286,7 +290,7 @@ public void WithConversationReference_WithNullConversation_ThrowsArgumentNullExc } [Fact] - public void WithConversationReference_WithNullFrom_ThrowsArgumentNullException() + public void WithConversationReference_WithNullFrom_SetsFromToRecipient() { CoreActivityBuilder builder = new(); CoreActivity sourceActivity = new() @@ -295,10 +299,12 @@ public void WithConversationReference_WithNullFrom_ThrowsArgumentNullException() ServiceUrl = new Uri("https://test.com"), Conversation = new Conversation(), From = null!, - Recipient = new ConversationAccount() + Recipient = new ConversationAccount { Id = "bot-1", Name = "Bot" } }; - Assert.Throws(() => builder.WithConversationReference(sourceActivity)); + CoreActivity activity = builder.WithConversationReference(sourceActivity).Build(); + Assert.NotNull(activity.From); + Assert.Equal("bot-1", activity.From.Id); } [Fact] @@ -333,17 +339,16 @@ public void WithConversationReference_AppliesConversationReference() .WithConversationReference(sourceActivity) .Build(); - Assert.Equal("msteams", activity.ChannelId); Assert.Equal(new Uri("https://smba.trafficmanager.net/teams/"), activity.ServiceUrl); Assert.Equal("conv-123", activity.Conversation.Id); + Assert.NotNull(activity.From); Assert.Equal("bot-1", activity.From.Id); Assert.Equal("Bot", activity.From.Name); - Assert.Equal("user-1", activity.Recipient.Id); - Assert.Equal("User One", activity.Recipient.Name); + Assert.Null(activity.Recipient); } [Fact] - public void WithConversationReference_SwapsFromAndRecipient() + public void WithConversationReference_SetsFromFromRecipient() { CoreActivity incomingActivity = new() { @@ -358,21 +363,21 @@ public void WithConversationReference_SwapsFromAndRecipient() .WithConversationReference(incomingActivity) .Build(); + Assert.NotNull(replyActivity.From); Assert.Equal("bot-id", replyActivity.From.Id); Assert.Equal("Bot", replyActivity.From.Name); - Assert.Equal("user-id", replyActivity.Recipient.Id); - Assert.Equal("User", replyActivity.Recipient.Name); + Assert.Null(replyActivity.Recipient); } [Fact] - public void WithChannelData_WithNullValue_SetsToNull() + public void WithChannelData_WithNullValue_DoesNotOverwrite() { CoreActivity activity = new CoreActivityBuilder() .WithChannelData(new ChannelData()) .WithChannelData(null) .Build(); - Assert.Null(activity.ChannelData); + Assert.NotNull(activity.ChannelData); } [Fact] @@ -423,8 +428,9 @@ public void WithConversationReference_ChainedWithOtherMethods_MaintainsFluentInt .Build(); Assert.Equal(ActivityType.Message, activity.Type); + Assert.NotNull(activity.From); Assert.Equal("bot-1", activity.From.Id); - Assert.Equal("user-1", activity.Recipient.Id); + Assert.Null(activity.Recipient); } [Fact] @@ -475,7 +481,9 @@ public void IntegrationTest_CreateComplexActivity() Assert.Equal("msg-001", activity.Id); Assert.Equal(serviceUrl, activity.ServiceUrl); Assert.Equal("msteams", activity.ChannelId); + Assert.NotNull(activity.From); Assert.Equal("bot-id", activity.From.Id); + Assert.NotNull(activity.Recipient); Assert.Equal("user-id", activity.Recipient.Id); Assert.Equal("conv-001", activity.Conversation.Id); Assert.NotNull(activity.ChannelData); diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/MiddlewareTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/MiddlewareTests.cs index be9d1fd6..614853dc 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/MiddlewareTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/MiddlewareTests.cs @@ -66,7 +66,8 @@ public async Task Middleware_ExecutesInOrder() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; @@ -112,7 +113,8 @@ public async Task Middleware_CanShortCircuit() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; @@ -156,7 +158,8 @@ public async Task Middleware_ReceivesCancellationToken() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; @@ -196,7 +199,8 @@ public async Task Middleware_ReceivesActivity() CoreActivity activity = new() { Type = ActivityType.Message, - Id = "act123" + Id = "act123", + Recipient = new ConversationAccount() }; activity.Recipient.Properties["appId"] = "test-app-id"; diff --git a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs index 36835f3b..05e4e831 100644 --- a/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs +++ b/core/test/Microsoft.Teams.Bot.Core.UnitTests/Schema/CoreActivityTests.cs @@ -115,7 +115,7 @@ public void Deserialize_Unkown__Fields_In_KnownObjects() Assert.Equal("message", act.Type); Assert.NotNull(act.From); Assert.IsType(act.From); - Assert.Equal("1", act.From!.Id); + Assert.Equal("1", act.From.Id); Assert.Equal("tester", act.From.Name); Assert.True(act.From.Properties.ContainsKey("aadObjectId")); Assert.Equal("123", act.From.Properties["aadObjectId"]?.ToString()); @@ -287,14 +287,13 @@ public void CreateReply() Assert.NotNull(reply); Assert.Equal(ActivityType.Message, reply.Type); Assert.Equal("reply", reply.Properties["text"]); - Assert.Equal("channel1", reply.ChannelId); Assert.NotNull(reply.ServiceUrl); Assert.Equal("http://service.url/", reply.ServiceUrl.ToString()); Assert.Equal("conversation1", reply.Conversation.Id); + Assert.NotNull(reply.From); Assert.Equal("bot1", reply.From.Id); Assert.Equal("Bot One", reply.From.Name); - Assert.Equal("user1", reply.Recipient.Id); - Assert.Equal("User One", reply.Recipient.Name); + Assert.Null(reply.Recipient); } [Fact]