diff --git a/core/samples/AllFeatures/Program.cs b/core/samples/AllFeatures/Program.cs index ff314f193..d0a41f437 100644 --- a/core/samples/AllFeatures/Program.cs +++ b/core/samples/AllFeatures/Program.cs @@ -14,8 +14,7 @@ await context.SendTypingActivityAsync(cancellationToken); - TeamsActivity reply = TeamsActivity.CreateBuilder() - .WithType(TeamsActivityType.Message) + MessageActivity reply = MessageActivity.CreateBuilder() .WithConversationReference(context.Activity) .WithText(replyText) .Build(); diff --git a/core/samples/CompatBot/EchoBot.cs b/core/samples/CompatBot/EchoBot.cs index 15876b53d..53e98db9d 100644 --- a/core/samples/CompatBot/EchoBot.cs +++ b/core/samples/CompatBot/EchoBot.cs @@ -44,7 +44,7 @@ protected override async Task OnMessageActivityAsync(ITurnContext(TeamsBotApplication botApplication, TActivity ac /// public Task SendActivityAsync(string text, CancellationToken cancellationToken = default) => TeamsBotApplication.SendActivityAsync( - new TeamsActivityBuilder() + MessageActivity.CreateBuilder() .WithConversationReference(Activity) .WithText(text) .Build(), cancellationToken); diff --git a/core/src/Microsoft.Teams.Bot.Apps/Handlers/InvokeHandler.cs b/core/src/Microsoft.Teams.Bot.Apps/Handlers/InvokeHandler.cs index dc092a9d2..5d3e60dc3 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Handlers/InvokeHandler.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Handlers/InvokeHandler.cs @@ -23,8 +23,7 @@ public static class InvokeExtensions { /// /// Registers a catch-all handler for all invoke activities. - /// Cannot be combined with specific invoke handlers such as , - /// , etc. + /// Cannot be combined with specific invoke handlers such as /// /// /// Breaking change: previously a catch-all invoke handler could be registered alongside specific invoke handlers. This combination now throws at registration time. diff --git a/core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskHandler.cs b/core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskModules/TaskHandler.cs similarity index 98% rename from core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskHandler.cs rename to core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskModules/TaskHandler.cs index 66c254671..bc1d5984e 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskHandler.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Handlers/TaskModules/TaskHandler.cs @@ -5,7 +5,7 @@ using Microsoft.Teams.Bot.Apps.Routing; using Microsoft.Teams.Bot.Apps.Schema; -namespace Microsoft.Teams.Bot.Apps.Handlers; +namespace Microsoft.Teams.Bot.Apps.Handlers.TaskModules; /// /// Delegate for handling task module invoke activities. diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/ProductInfoEntity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/ProductInfoEntity.cs index 3d4a971e0..875284d5e 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/ProductInfoEntity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/ProductInfoEntity.cs @@ -2,9 +2,43 @@ // Licensed under the MIT License. using System.Text.Json.Serialization; +using Microsoft.Teams.Bot.Apps.Schema; namespace Microsoft.Teams.Bot.Apps.Schema.Entities; +/// +/// Extension methods for activity product info. +/// +public static class ActivityProductInfoExtensions +{ + /// + /// Adds a product info entity to the activity. + /// + /// The activity to add product info to. Cannot be null. + /// The product identifier. + /// The created ProductInfoEntity that was added to the activity. + public static ProductInfoEntity AddProductInfo(this TeamsActivity activity, string id) + { + ArgumentNullException.ThrowIfNull(activity); + ProductInfoEntity productInfo = new() { Id = id }; + activity.Entities ??= []; + activity.Entities.Add(productInfo); + activity.Rebase(); + return productInfo; + } + + /// + /// Gets the product info entity from the activity's entity collection, if present. + /// + /// The activity to read from. Cannot be null. + /// The ProductInfoEntity if found; otherwise, null. + public static ProductInfoEntity? GetProductInfo(this TeamsActivity activity) + { + ArgumentNullException.ThrowIfNull(activity); + return activity.Entities?.FirstOrDefault(e => e is ProductInfoEntity) as ProductInfoEntity; + } +} + diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/StreamInfoEntity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/StreamInfoEntity.cs index 614604dc7..20cd7f22a 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/StreamInfoEntity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/Entities/StreamInfoEntity.cs @@ -2,9 +2,50 @@ // Licensed under the MIT License. using System.Text.Json.Serialization; +using Microsoft.Teams.Bot.Apps.Schema; namespace Microsoft.Teams.Bot.Apps.Schema.Entities; +/// +/// Extension methods for activity stream info. +/// +public static class ActivityStreamInfoExtensions +{ + /// + /// Adds a stream info entity to the activity. + /// + /// The activity to add stream info to. Cannot be null. + /// The stream type. See for possible values. + /// Optional stream identifier. + /// Optional stream sequence number. + /// The created StreamInfoEntity that was added to the activity. + public static StreamInfoEntity AddStreamInfo(this TeamsActivity activity, string streamType, string? streamId = null, int? streamSequence = null) + { + ArgumentNullException.ThrowIfNull(activity); + StreamInfoEntity streamInfo = new() + { + StreamType = streamType, + StreamId = streamId, + StreamSequence = streamSequence + }; + activity.Entities ??= []; + activity.Entities.Add(streamInfo); + activity.Rebase(); + return streamInfo; + } + + /// + /// Gets the stream info entity from the activity's entity collection, if present. + /// + /// The activity to read from. Cannot be null. + /// The StreamInfoEntity if found; otherwise, null. + public static StreamInfoEntity? GetStreamInfo(this TeamsActivity activity) + { + ArgumentNullException.ThrowIfNull(activity); + return activity.Entities?.FirstOrDefault(e => e is StreamInfoEntity) as StreamInfoEntity; + } +} + /// /// Stream info entity. /// diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.Builder.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.Builder.cs new file mode 100644 index 000000000..fd3cdd222 --- /dev/null +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.Builder.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Bot.Apps.Schema.Entities; +using Microsoft.Teams.Bot.Core.Schema; + +namespace Microsoft.Teams.Bot.Apps.Schema; + +/// +/// Provides a fluent API for building instances. +/// Uses typed property setters for first-class message fields (Text, TextFormat, SuggestedActions). +/// +public class MessageActivityBuilder : TeamsActivityBuilder +{ + /// + /// Initializes a new instance of the MessageActivityBuilder class. + /// + internal MessageActivityBuilder() : base(new MessageActivity()) + { + } + + /// + /// Initializes a new instance of the MessageActivityBuilder class with an existing activity. + /// + /// The activity to build upon. + internal MessageActivityBuilder(MessageActivity activity) : base(activity) + { + } + + /// + /// Sets the text content and text format of the message. + /// + public MessageActivityBuilder WithText(string text, string textFormat = "plain") + { + _activity.Text = text; + _activity.TextFormat = textFormat; + return this; + } + + /// + /// Sets the suggested actions for the message. + /// + public MessageActivityBuilder WithSuggestedActions(SuggestedActions suggestedActions) + { + _activity.SuggestedActions = suggestedActions; + return this; + } + + /// + /// Adds a mention to the activity. + /// + /// The account to mention. + /// Optional custom text for the mention. If null, uses the account name. + /// Whether to prepend the mention text to the activity's text content. + public MessageActivityBuilder AddMention(ConversationAccount account, string? text = null, bool addText = true) + { + ArgumentNullException.ThrowIfNull(account); + string? mentionText = text ?? account.Name; + + if (addText) + { + _activity.Text = $"{mentionText} {_activity.Text}"; + } + + _activity.Entities ??= []; + _activity.Entities.Add(new MentionEntity(account, $"{mentionText}")); + + return this; + } + + /// + /// Builds and returns the configured MessageActivity instance. + /// + public override MessageActivity Build() + { + _activity.Rebase(); + return _activity; + } +} diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.cs index 7ba071aa0..6023a9067 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivity.cs @@ -78,10 +78,6 @@ protected MessageActivity(CoreActivity activity) : base(activity) { SuggestedActions = JsonSerializer.Deserialize(je.GetRawText()); } - else - { - SuggestedActions = suggestedActions as SuggestedActions; - } activity.Properties.Remove("suggestedActions"); } /* @@ -135,7 +131,21 @@ protected MessageActivity(CoreActivity activity) : base(activity) [JsonPropertyName("attachmentLayout")] public string? AttachmentLayout { get; set; } - + /// + /// Gets or sets the suggested actions for the message. + /// + [JsonPropertyName("suggestedActions")] + public SuggestedActions? SuggestedActions { get; set; } + + /// + /// Creates a new instance. + /// + public static new MessageActivityBuilder CreateBuilder() => new(); + + /// + /// Creates a new initialized with an existing activity. + /// + public static MessageActivityBuilder CreateBuilder(MessageActivity activity) => new(activity); //TODO : Review properties /* diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.Builder.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.Builder.cs new file mode 100644 index 000000000..b544bec01 --- /dev/null +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.Builder.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Teams.Bot.Apps.Schema; + +/// +/// Provides a fluent API for building instances. +/// +public class StreamingActivityBuilder : TeamsActivityBuilder +{ + /// + /// Initializes a new instance of the StreamingActivityBuilder class with an initial text chunk. + /// + /// The initial text content of the streaming chunk. + internal StreamingActivityBuilder(string text = "") : base(new StreamingActivity(text)) + { + } + + /// + /// Sets the text content of the streaming chunk. + /// + public StreamingActivityBuilder WithText(string text) + { + _activity.Text = text; + return this; + } + + /// + /// Builds and returns the configured StreamingActivity instance. + /// + public override StreamingActivity Build() + { + _activity.Rebase(); + return _activity; + } +} diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.cs index 547123fe8..1b142b34d 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/StreamingActivity.cs @@ -21,7 +21,8 @@ public StreamingActivity(string text) : base(TeamsActivityType.Typing) { Text = text; StreamInfo = new StreamInfoEntity(); - AddEntity(StreamInfo); + Entities ??= []; + Entities.Add(StreamInfo); } /// @@ -34,4 +35,9 @@ public StreamingActivity(string text) : base(TeamsActivityType.Typing) /// Gets the stream info entity for this streaming activity. /// public StreamInfoEntity StreamInfo { get; } + + /// + /// Creates a new with an initial text chunk. + /// + public static StreamingActivityBuilder CreateBuilder(string text = "") => new(text); } diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs index 0f0646651..0a3ff7e73 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivity.cs @@ -65,7 +65,7 @@ protected TeamsActivity(CoreActivity activity) : base(activity) // Convert base types to Teams-specific types if (activity.ChannelData is not null) { - ChannelData = new TeamsChannelData(activity.ChannelData); + ChannelData = TeamsChannelData.FromChannelData(activity.ChannelData); } if (activity.From is not null) @@ -146,6 +146,7 @@ internal TeamsActivity Rebase() /// [JsonPropertyName("entities")] public new EntityList? Entities { get; set; } + // TODO : mv attachments to MessageActivity if it's only used for messages. /// /// Attachments specific to Teams. /// @@ -175,27 +176,6 @@ internal TeamsActivity Rebase() [JsonPropertyName("localTimezone")] public string? LocalTimezone { get; set; } - /// - /// Gets or sets the suggested actions for the message. - /// - [JsonPropertyName("suggestedActions")] - public SuggestedActions? SuggestedActions { get; set; } - - - /// - /// Adds an entity to the activity's Entities collection. - /// - /// - /// - public TeamsActivity AddEntity(Entity entity) - { - // TODO: Pick up nuances about entities. - // For eg, there can only be 1 single MessageEntity - Entities ??= []; - Entities.Add(entity); - return this; - } - /// /// Creates a new TeamsActivityBuilder instance for building a TeamsActivity with a fluent API. /// diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivityBuilder.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivityBuilder.cs index d48ba3e84..b80849a29 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsActivityBuilder.cs @@ -7,28 +7,23 @@ namespace Microsoft.Teams.Bot.Apps.Schema; /// -/// Provides a fluent API for building TeamsActivity instances. +/// Abstract generic base for Teams activity builders. +/// Provides Teams-specific overrides and fluent methods common to all Teams activity types. /// -public class TeamsActivityBuilder : CoreActivityBuilder +/// The concrete Teams activity type being built. +/// The concrete builder type (for fluent chaining). +public abstract class TeamsActivityBuilder : CoreActivityBuilder + where TActivity : TeamsActivity + where TBuilder : TeamsActivityBuilder { /// - /// Initializes a new instance of the TeamsActivityBuilder class. - /// - internal TeamsActivityBuilder() : base(new TeamsActivity()) - { - } - - /// - /// Initializes a new instance of the TeamsActivityBuilder class with an existing activity. + /// Initializes a new instance with the given activity. /// - /// The activity to build upon. - internal TeamsActivityBuilder(TeamsActivity activity) : base(activity) + protected TeamsActivityBuilder(TActivity activity) : base(activity) { } - /// - /// Sets the conversation (override for Teams-specific type). - /// + /// protected override void SetConversation(Conversation? conversation) { _activity.Conversation = conversation is TeamsConversation teamsConv @@ -36,9 +31,7 @@ protected override void SetConversation(Conversation? conversation) : TeamsConversation.FromConversation(conversation); } - /// - /// Sets the From account (override for Teams-specific type). - /// + /// protected override void SetFrom(ConversationAccount? from) { _activity.From = from is TeamsConversationAccount teamsAccount @@ -46,9 +39,7 @@ protected override void SetFrom(ConversationAccount? from) : TeamsConversationAccount.FromConversationAccount(from); } - /// - /// Sets the Recipient account (override for Teams-specific type). - /// + /// protected override void SetRecipient(ConversationAccount? recipient) { _activity.Recipient = recipient is TeamsConversationAccount teamsAccount @@ -59,172 +50,100 @@ protected override void SetRecipient(ConversationAccount? recipient) /// /// Sets the Teams-specific channel data. /// - /// The channel data. - /// The builder instance for chaining. - public TeamsActivityBuilder WithChannelData(TeamsChannelData? channelData) + public TBuilder WithChannelData(TeamsChannelData? channelData) { _activity.ChannelData = channelData; - return this; + return (TBuilder)this; } /// /// Sets the entities collection. /// - /// The entities collection. - /// The builder instance for chaining. - public TeamsActivityBuilder WithEntities(EntityList entities) + public TBuilder WithEntities(EntityList entities) { _activity.Entities = entities; - return this; + return (TBuilder)this; } /// - /// Sets the attachments collection. + /// Adds an entity to the activity's Entities collection. /// - /// The attachments collection. - /// The builder instance for chaining. - public TeamsActivityBuilder WithAttachments(IList attachments) + public TBuilder AddEntity(Entity entity) { - _activity.Attachments = attachments; - return this; + _activity.Entities ??= []; + _activity.Entities.Add(entity); + return (TBuilder)this; } - // TODO: Builders should only have "With" methods, not "Add" methods. /// - /// Replaces the attachments collection with a single attachment. + /// Sets the attachments collection. /// - /// The attachment to set. Passing null clears the attachments. - /// The builder instance for chaining. - public TeamsActivityBuilder WithAttachment(TeamsAttachment? attachment) + public TBuilder WithAttachments(IList attachments) { - _activity.Attachments = attachment is null - ? null - : [attachment]; - - return this; + _activity.Attachments = attachments; + return (TBuilder)this; } - /// - /// Adds an entity to the activity's Entities collection. - /// - /// The entity to add. - /// The builder instance for chaining. - public TeamsActivityBuilder AddEntity(Entity entity) - { - _activity.Entities ??= []; - _activity.Entities.Add(entity); - return this; - } /// /// Adds an attachment to the activity's Attachments collection. /// - /// The attachment to add. - /// The builder instance for chaining. - public TeamsActivityBuilder AddAttachment(TeamsAttachment attachment) + public TBuilder AddAttachment(TeamsAttachment attachment) { _activity.Attachments ??= []; _activity.Attachments.Add(attachment); - return this; + return (TBuilder)this; } /// /// Adds an Adaptive Card attachment to the activity. /// /// The Adaptive Card payload. - /// Optional callback to further configure the attachment before it is added. - /// The builder instance for chaining. - public TeamsActivityBuilder AddAdaptiveCardAttachment(object adaptiveCard, Action? configure = null) - { - TeamsAttachment attachment = BuildAdaptiveCardAttachment(adaptiveCard, configure); - return AddAttachment(attachment); - } - - /// - /// Sets the activity attachments collection to a single Adaptive Card attachment. - /// - /// The Adaptive Card payload. /// Optional callback to further configure the attachment. - /// The builder instance for chaining. - public TeamsActivityBuilder WithAdaptiveCardAttachment(object adaptiveCard, Action? configure = null) + public TBuilder AddAdaptiveCardAttachment(object adaptiveCard, Action? configure = null) { - TeamsAttachment attachment = BuildAdaptiveCardAttachment(adaptiveCard, configure); - return WithAttachment(attachment); + ArgumentNullException.ThrowIfNull(adaptiveCard); + return AddAttachment(BuildAdaptiveCardAttachment(adaptiveCard, configure)); } - /// - /// Adds or sets the text content of the activity. - /// - /// - /// - /// - public TeamsActivityBuilder WithText(string text, string textFormat = "plain") + private static TeamsAttachment BuildAdaptiveCardAttachment(object adaptiveCard, Action? configure) { - WithProperty("text", text); - WithProperty("textFormat", textFormat); - return this; + TeamsAttachmentBuilder attachmentBuilder = TeamsAttachment + .CreateBuilder() + .WithAdaptiveCard(adaptiveCard); + + configure?.Invoke(attachmentBuilder); + + return attachmentBuilder.Build(); } +} +/// +/// Provides a fluent API for building instances. +/// +public class TeamsActivityBuilder : TeamsActivityBuilder +{ /// - /// With Suggested Actions + /// Initializes a new instance of the TeamsActivityBuilder class. /// - /// - /// - public TeamsActivityBuilder WithSuggestedActions(SuggestedActions suggestedActions) + internal TeamsActivityBuilder() : base(new TeamsActivity()) { - ArgumentNullException.ThrowIfNull(_activity); - _activity.SuggestedActions = suggestedActions; - return this; } /// - /// Adds a mention to the activity. + /// Initializes a new instance of the TeamsActivityBuilder class with an existing activity. /// - /// The account to mention. - /// Optional custom text for the mention. If null, uses the account name. - /// Whether to prepend the mention text to the activity's text content. - /// The builder instance for chaining. - public TeamsActivityBuilder AddMention(ConversationAccount account, string? text = null, bool addText = true) + /// The activity to build upon. + internal TeamsActivityBuilder(TeamsActivity activity) : base(activity) { - ArgumentNullException.ThrowIfNull(account); - string? mentionText = text ?? account.Name; - - if (addText) - { - string? currentText = _activity.Properties.TryGetValue("text", out object? value) ? value?.ToString() : null; - WithProperty("text", $"{mentionText} {currentText}"); - } - - _activity.Entities ??= []; - _activity.Entities.Add(new MentionEntity(account, $"{mentionText}")); - - CoreActivity baseActivity = _activity; - baseActivity.Entities = _activity.Entities.ToJsonArray(); - - return this; } /// /// Builds and returns the configured TeamsActivity instance. /// - /// The configured TeamsActivity. public override TeamsActivity Build() { _activity.Rebase(); - return _activity; } - - private static TeamsAttachment BuildAdaptiveCardAttachment(object adaptiveCard, Action? configure) - { - ArgumentNullException.ThrowIfNull(adaptiveCard); - - TeamsAttachmentBuilder attachmentBuilder = TeamsAttachment - .CreateBuilder() - .WithAdaptiveCard(adaptiveCard); - - configure?.Invoke(attachmentBuilder); - - return attachmentBuilder.Build(); - } } diff --git a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsChannelData.cs b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsChannelData.cs index 93bed673b..06cb40e9f 100644 --- a/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsChannelData.cs +++ b/core/src/Microsoft.Teams.Bot.Apps/Schema/TeamsChannelData.cs @@ -65,74 +65,79 @@ public TeamsChannelData() /// Creates a new instance of the class from the specified object. /// /// - public TeamsChannelData(ChannelData? cd) + public static TeamsChannelData? FromChannelData(ChannelData? cd) { - if (cd is not null) + if (cd is null) { - //TODO : is channel id needed ? what is teamschannleid and teamsteamid ? - if (cd.Properties.TryGetValue("teamsChannelId", out object? channelIdObj) - && channelIdObj is JsonElement jeChannelId - && jeChannelId.ValueKind == JsonValueKind.String) - { - TeamsChannelId = jeChannelId.GetString(); - } - - if (cd.Properties.TryGetValue("teamsTeamId", out object? teamIdObj) - && teamIdObj is JsonElement jeTeamId - && jeTeamId.ValueKind == JsonValueKind.String) - { - TeamsTeamId = jeTeamId.GetString(); - } - - if (cd.Properties.TryGetValue("settings", out object? settingsObj) - && settingsObj is JsonElement settingsObjJE - && settingsObjJE.ValueKind == JsonValueKind.Object) - { - Settings = JsonSerializer.Deserialize(settingsObjJE.GetRawText()); - } - - if (cd.Properties.TryGetValue("channel", out object? channelObj) - && channelObj is JsonElement channelObjJE - && channelObjJE.ValueKind == JsonValueKind.Object) - { - Channel = JsonSerializer.Deserialize(channelObjJE.GetRawText()); - } - - if (cd.Properties.TryGetValue("tenant", out object? tenantObj) - && tenantObj is JsonElement je - && je.ValueKind == JsonValueKind.Object) - { - Tenant = JsonSerializer.Deserialize(je.GetRawText()); - } - - if (cd.Properties.TryGetValue("eventType", out object? eventTypeObj) - && eventTypeObj is JsonElement jeEventType - && jeEventType.ValueKind == JsonValueKind.String) - { - EventType = jeEventType.GetString(); - } - - if (cd.Properties.TryGetValue("team", out object? teamObj) - && teamObj is JsonElement teamObjJE - && teamObjJE.ValueKind == JsonValueKind.Object) - { - Team = JsonSerializer.Deserialize(teamObjJE.GetRawText()); - } - - if (cd.Properties.TryGetValue("source", out object? sourceObj) - && sourceObj is JsonElement sourceObjJE - && sourceObjJE.ValueKind == JsonValueKind.Object) - { - Source = JsonSerializer.Deserialize(sourceObjJE.GetRawText()); - } - - if (cd.Properties.TryGetValue("feedbackLoopEnabled", out object? feedbackObj) - && feedbackObj is JsonElement jeFeedback - && jeFeedback.ValueKind is JsonValueKind.True or JsonValueKind.False) - { - FeedbackLoopEnabled = jeFeedback.GetBoolean(); - } + return null; } + + TeamsChannelData result = new(); + + //TODO : is channel id needed ? what is teamschannleid and teamsteamid ? + if (cd.Properties.TryGetValue("teamsChannelId", out object? channelIdObj) + && channelIdObj is JsonElement jeChannelId + && jeChannelId.ValueKind == JsonValueKind.String) + { + result.TeamsChannelId = jeChannelId.GetString(); + } + + if (cd.Properties.TryGetValue("teamsTeamId", out object? teamIdObj) + && teamIdObj is JsonElement jeTeamId + && jeTeamId.ValueKind == JsonValueKind.String) + { + result.TeamsTeamId = jeTeamId.GetString(); + } + + if (cd.Properties.TryGetValue("settings", out object? settingsObj) + && settingsObj is JsonElement settingsObjJE + && settingsObjJE.ValueKind == JsonValueKind.Object) + { + result.Settings = JsonSerializer.Deserialize(settingsObjJE.GetRawText()); + } + + if (cd.Properties.TryGetValue("channel", out object? channelObj) + && channelObj is JsonElement channelObjJE + && channelObjJE.ValueKind == JsonValueKind.Object) + { + result.Channel = JsonSerializer.Deserialize(channelObjJE.GetRawText()); + } + + if (cd.Properties.TryGetValue("tenant", out object? tenantObj) + && tenantObj is JsonElement je + && je.ValueKind == JsonValueKind.Object) + { + result.Tenant = JsonSerializer.Deserialize(je.GetRawText()); + } + + if (cd.Properties.TryGetValue("eventType", out object? eventTypeObj) + && eventTypeObj is JsonElement jeEventType + && jeEventType.ValueKind == JsonValueKind.String) + { + result.EventType = jeEventType.GetString(); + } + + if (cd.Properties.TryGetValue("team", out object? teamObj) + && teamObj is JsonElement teamObjJE + && teamObjJE.ValueKind == JsonValueKind.Object) + { + result.Team = JsonSerializer.Deserialize(teamObjJE.GetRawText()); + } + + if (cd.Properties.TryGetValue("source", out object? sourceObj) + && sourceObj is JsonElement sourceObjJE + && sourceObjJE.ValueKind == JsonValueKind.Object) + { + result.Source = JsonSerializer.Deserialize(sourceObjJE.GetRawText()); + } + + if (cd.Properties.TryGetValue("feedbackLoopEnabled", out object? feedbackObj) + && feedbackObj is JsonElement jeFeedback + && jeFeedback.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + result.FeedbackLoopEnabled = jeFeedback.GetBoolean(); + } + return result; } diff --git a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs index 3dd53f934..48db00db7 100644 --- a/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs @@ -34,7 +34,7 @@ protected CoreActivityBuilder(TActivity activity) /// /// The source activity to copy conversation reference from. /// The builder instance for chaining. - public TBuilder WithConversationReference(TActivity activity) + public TBuilder WithConversationReference(CoreActivity activity) { ArgumentNullException.ThrowIfNull(activity); ArgumentNullException.ThrowIfNull(activity.ChannelId); diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/MessageActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/MessageActivityBuilderTests.cs new file mode 100644 index 000000000..9d312ffcf --- /dev/null +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/MessageActivityBuilderTests.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Bot.Apps.Schema; +using Microsoft.Teams.Bot.Apps.Schema.Entities; +using Microsoft.Teams.Bot.Core.Schema; + +namespace Microsoft.Teams.Bot.Apps.UnitTests; + +public class MessageActivityBuilderTests +{ + [Fact] + public void WithText_SetsTextAndFormat() + { + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("Hello, World!") + .Build(); + + Assert.Equal("Hello, World!", activity.Text); + Assert.Equal("plain", activity.TextFormat); + } + + [Fact] + public void WithText_CustomFormat_SetsFormat() + { + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("**bold**", "markdown") + .Build(); + + Assert.Equal("**bold**", activity.Text); + Assert.Equal("markdown", activity.TextFormat); + } + + [Fact] + public void WithSuggestedActions_SetsSuggestedActions() + { + SuggestedActions suggestedActions = new() + { + Actions = [new() { Title = "Yes", Type = "imBack", Value = "yes" }] + }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithSuggestedActions(suggestedActions) + .Build(); + + Assert.NotNull(activity.SuggestedActions); + Assert.Single(activity.SuggestedActions.Actions!); + Assert.Equal("Yes", activity.SuggestedActions.Actions![0].Title); + } + + [Fact] + public void AddMention_WithNullAccount_ThrowsArgumentNullException() + { + Assert.Throws(() => MessageActivity.CreateBuilder().AddMention(null!)); + } + + [Fact] + public void AddMention_WithAccountAndDefaultText_AddsMentionAndUpdatesText() + { + ConversationAccount account = new() + { + Id = "user-123", + Name = "John Doe" + }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("said hello") + .AddMention(account) + .Build(); + + Assert.Equal("John Doe said hello", activity.Text); + Assert.NotNull(activity.Entities); + Assert.Single(activity.Entities); + + MentionEntity? mention = activity.Entities[0] as MentionEntity; + Assert.NotNull(mention); + Assert.Equal("user-123", mention.Mentioned?.Id); + Assert.Equal("John Doe", mention.Mentioned?.Name); + Assert.Equal("John Doe", mention.Text); + } + + [Fact] + public void AddMention_WithCustomText_UsesCustomText() + { + ConversationAccount account = new() + { + Id = "user-123", + Name = "John Doe" + }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("replied") + .AddMention(account, "CustomName") + .Build(); + + Assert.Equal("CustomName replied", activity.Text); + + MentionEntity? mention = activity.Entities![0] as MentionEntity; + Assert.NotNull(mention); + Assert.Equal("CustomName", mention.Text); + } + + [Fact] + public void AddMention_WithAddTextFalse_DoesNotUpdateText() + { + ConversationAccount account = new() + { + Id = "user-123", + Name = "John Doe" + }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("original text") + .AddMention(account, addText: false) + .Build(); + + Assert.Equal("original text", activity.Text); + Assert.NotNull(activity.Entities); + Assert.Single(activity.Entities); + } + + [Fact] + public void AddMention_MultipleMentions_AddsAllMentions() + { + ConversationAccount account1 = new() { Id = "user-1", Name = "User One" }; + ConversationAccount account2 = new() { Id = "user-2", Name = "User Two" }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("message") + .AddMention(account1) + .AddMention(account2) + .Build(); + + Assert.Equal("User Two User One message", activity.Text); + Assert.NotNull(activity.Entities); + Assert.Equal(2, activity.Entities?.Count); + } + + [Fact] + public void AddMention_EmptyText_PrependsMention() + { + ConversationAccount account = new() { Id = "user-123", Name = "User" }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .AddMention(account) + .Build(); + + Assert.Equal("User ", activity.Text); + } + + [Fact] + public void AddMention_WithAccountWithNullName_UsesNullText() + { + ConversationAccount account = new() { Id = "user-123", Name = null }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithText("message") + .AddMention(account) + .Build(); + + Assert.Equal(" message", activity.Text); + Assert.NotNull(activity.Entities); + Assert.Single(activity.Entities); + } + + [Fact] + public void AddMention_UpdatesBaseEntityCollection() + { + ConversationAccount account = new() { Id = "user-123", Name = "Test User" }; + + MessageActivity activity = MessageActivity.CreateBuilder() + .AddMention(account) + .Build(); + + CoreActivity baseActivity = activity; + Assert.NotNull(baseActivity.Entities); + Assert.NotEmpty(baseActivity.Entities); + } + + [Fact] + public void MethodChaining_ReturnsBuilderInstance() + { + MessageActivityBuilder msgBuilder = MessageActivity.CreateBuilder(); + + MessageActivityBuilder result1 = msgBuilder.WithId("id"); + MessageActivityBuilder result2 = msgBuilder.WithText("text"); + MessageActivityBuilder result3 = msgBuilder.WithType(TeamsActivityType.Message); + + Assert.Same(msgBuilder, result1); + Assert.Same(msgBuilder, result2); + Assert.Same(msgBuilder, result3); + } + + [Fact] + public void CreateBuilder_WithExistingActivity_PreservesData() + { + MessageActivity original = new() { Id = "original-id", Text = "original text" }; + + MessageActivity modified = MessageActivity.CreateBuilder(original) + .WithText("modified text") + .Build(); + + Assert.Equal("original-id", modified.Id); + Assert.Equal("modified text", modified.Text); + } + + [Fact] + public void IntegrationTest_CreateComplexMessageActivity() + { + Uri serviceUrl = new("https://smba.trafficmanager.net/amer/test/"); + TeamsChannelData channelData = new() + { + TeamsChannelId = "19:channel@thread.tacv2", + TeamsTeamId = "19:team@thread.tacv2" + }; + + Conversation conv = new() + { + Id = "conv-001", + Properties = + { + { "tenantId", "tenant-001" }, + { "conversationType", "channel" } + } + }; + + TeamsConversation? tc = TeamsConversation.FromConversation(conv); + Assert.NotNull(tc); + + MessageActivity activity = MessageActivity.CreateBuilder() + .WithId("msg-001") + .WithServiceUrl(serviceUrl) + .WithChannelId("msteams") + .WithText("Please review this document") + .WithFrom(TeamsConversationAccount.FromConversationAccount(new ConversationAccount { Id = "bot-id", Name = "Bot" })) + .WithRecipient(TeamsConversationAccount.FromConversationAccount(new ConversationAccount { Id = "user-id", Name = "User" })) + .WithConversation(tc) + .WithChannelData(channelData) + .AddEntity(new ClientInfoEntity { Locale = "en-US", Country = "US", Platform = "Web" }) + .AddAttachment(new TeamsAttachment { ContentType = "application/vnd.microsoft.card.adaptive", Name = "card.json" }) + .AddMention(new ConversationAccount { Id = "manager-id", Name = "Manager" }, "Manager") + .Build(); + + Assert.Equal(TeamsActivityType.Message, activity.Type); + Assert.Equal("msg-001", activity.Id); + Assert.Equal(serviceUrl, activity.ServiceUrl); + Assert.Equal("msteams", activity.ChannelId); + Assert.Equal("Manager Please review this document", activity.Text); + Assert.Equal("bot-id", activity.From?.Id); + Assert.Equal("user-id", activity.Recipient?.Id); + Assert.Equal("conv-001", activity.Conversation?.Id); + Assert.Equal("tenant-001", activity.Conversation?.TenantId); + Assert.Equal("channel", activity.Conversation?.ConversationType); + Assert.NotNull(activity.ChannelData); + Assert.Equal("19:channel@thread.tacv2", activity.ChannelData?.TeamsChannelId); + Assert.NotNull(activity.Entities); + Assert.Equal(2, activity.Entities?.Count); // ClientInfo + Mention + Assert.NotNull(activity.Attachments); + Assert.Single(activity.Attachments); + } +} diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/StreamingActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/StreamingActivityBuilderTests.cs new file mode 100644 index 000000000..c714f5cb2 --- /dev/null +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/StreamingActivityBuilderTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Bot.Apps.Schema; +using Microsoft.Teams.Bot.Apps.Schema.Entities; + +namespace Microsoft.Teams.Bot.Apps.UnitTests; + +public class StreamingActivityBuilderTests +{ + [Fact] + public void CreateBuilder_DefaultText_CreatesStreamingActivity() + { + StreamingActivity activity = StreamingActivity.CreateBuilder().Build(); + + Assert.NotNull(activity); + Assert.Equal(TeamsActivityType.Typing, activity.Type); + Assert.Equal("", activity.Text); + } + + [Fact] + public void CreateBuilder_WithInitialText_SetsText() + { + StreamingActivity activity = StreamingActivity.CreateBuilder("Hello").Build(); + + Assert.Equal("Hello", activity.Text); + } + + [Fact] + public void WithText_UpdatesText() + { + StreamingActivity activity = StreamingActivity.CreateBuilder("initial") + .WithText("updated text") + .Build(); + + Assert.Equal("updated text", activity.Text); + } + + [Fact] + public void Build_ActivityHasStreamInfoEntity() + { + StreamingActivity activity = StreamingActivity.CreateBuilder("chunk").Build(); + + Assert.NotNull(activity.StreamInfo); + Assert.NotNull(activity.Entities); + Assert.Contains(activity.Entities, e => e is StreamInfoEntity); + } + + [Fact] + public void MethodChaining_ReturnsBuilderInstance() + { + StreamingActivityBuilder streamBuilder = StreamingActivity.CreateBuilder(); + + StreamingActivityBuilder result1 = streamBuilder.WithId("id"); + StreamingActivityBuilder result2 = streamBuilder.WithText("text"); + + Assert.Same(streamBuilder, result1); + Assert.Same(streamBuilder, result2); + } + + [Fact] + public void Build_ReturnsStreamingActivity() + { + StreamingActivity activity = StreamingActivity.CreateBuilder("test").Build(); + + Assert.IsType(activity); + } +} diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/SuggestedActionsTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/SuggestedActionsTests.cs index 2610677e3..774357ac3 100644 --- a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/SuggestedActionsTests.cs +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/SuggestedActionsTests.cs @@ -191,8 +191,7 @@ public void MessageActivity_WithSuggestedActions_SetsProperty() { var suggestedActions = new SuggestedActions(); - var activity = TeamsActivity.CreateBuilder() - .WithType(TeamsActivityType.Message) + MessageActivity activity = MessageActivity.CreateBuilder() .WithText("Choose an option") .WithSuggestedActions(suggestedActions) .Build(); @@ -202,7 +201,7 @@ public void MessageActivity_WithSuggestedActions_SetsProperty() Assert.Empty(activity.SuggestedActions.Actions); } - + [Fact] public void MessageActivity_WithSuggestedActions() @@ -210,8 +209,7 @@ public void MessageActivity_WithSuggestedActions() var suggestedActions = new SuggestedActions() .AddAction(new SuggestedAction(ActionType.IMBack, "Option 1") { Value = "opt1" }); - var activity = TeamsActivity.CreateBuilder() - .WithType(TeamsActivityType.Message) + MessageActivity activity = MessageActivity.CreateBuilder() .WithText("Choose an option") .WithSuggestedActions(suggestedActions) .Build(); diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs index d88d449eb..4939e7089 100644 --- a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityBuilderTests.cs @@ -90,16 +90,6 @@ public void WithType_SetsActivityType() Assert.Equal(TeamsActivityType.Message, activity.Type); } - [Fact] - public void WithText_SetsTextContent() - { - TeamsActivity activity = builder - .WithText("Hello, World!") - .Build(); - - Assert.Equal("Hello, World!", activity.Properties["text"]); - } - [Fact] public void WithFrom_SetsSenderAccount() { @@ -138,8 +128,6 @@ public void WithRecipient_SetsRecipientAccount() public void WithConversation_SetsConversationInfo() { Conversation baseConversation = new Conversation("conversation-id"); - - Assert.NotNull(baseConversation); baseConversation.Properties.Add("tenantId", "tenant-123"); baseConversation.Properties.Add("conversationType", "channel"); TeamsConversation? conversation = TeamsConversation.FromConversation(baseConversation); @@ -213,24 +201,6 @@ public void WithAttachments_SetsAttachmentsCollection() Assert.Equal("test-attachment", activity.Attachments[0].Name); } - [Fact] - public void WithAttachment_SetsSingleAttachment() - { - TeamsAttachment attachment = new() - { - ContentType = "application/json", - Name = "single" - }; - - TeamsActivity activity = builder - .WithAttachment(attachment) - .Build(); - - Assert.NotNull(activity.Attachments); - Assert.Single(activity.Attachments); - Assert.Equal("single", activity.Attachments[0].Name); - } - [Fact] public void AddEntity_AddsEntityToCollection() { @@ -307,12 +277,12 @@ public void AddAdaptiveCardAttachment_AddsAdaptiveCard() } [Fact] - public void WithAdaptiveCardAttachment_ConfigureActionAppliesChanges() + public void AddAdaptiveCardAttachment_WithConfigure_AppliesChanges() { var adaptiveCard = new { type = "AdaptiveCard" }; TeamsActivity activity = builder - .WithAdaptiveCardAttachment(adaptiveCard, b => b.WithName("feedback")) + .AddAdaptiveCardAttachment(adaptiveCard, b => b.WithName("feedback")) .Build(); Assert.NotNull(activity.Attachments); @@ -326,153 +296,20 @@ public void AddAdaptiveCardAttachment_WithNullPayload_Throws() Assert.Throws(() => builder.AddAdaptiveCardAttachment(null!)); } - [Fact] - public void AddMention_WithNullAccount_ThrowsArgumentNullException() - { - Assert.Throws(() => builder.AddMention(null!)); - } - - [Fact] - public void AddMention_WithAccountAndDefaultText_AddsMentionAndUpdatesText() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = "John Doe" - }; - - TeamsActivity activity = builder - .WithText("said hello") - .AddMention(account) - .Build(); - - Assert.Equal("John Doe said hello", activity.Properties["text"]); - Assert.NotNull(activity.Entities); - Assert.Single(activity.Entities); - - MentionEntity? mention = activity.Entities[0] as MentionEntity; - Assert.NotNull(mention); - Assert.Equal("user-123", mention.Mentioned?.Id); - Assert.Equal("John Doe", mention.Mentioned?.Name); - Assert.Equal("John Doe", mention.Text); - } - - [Fact] - public void AddMention_WithCustomText_UsesCustomText() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = "John Doe" - }; - - TeamsActivity activity = builder - .WithText("replied") - .AddMention(account, "CustomName") - .Build(); - - Assert.Equal("CustomName replied", activity.Properties["text"]); - - MentionEntity? mention = activity.Entities![0] as MentionEntity; - Assert.NotNull(mention); - Assert.Equal("CustomName", mention.Text); - } - - [Fact] - public void AddMention_WithAddTextFalse_DoesNotUpdateText() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = "John Doe" - }; - - TeamsActivity activity = builder - .WithText("original text") - .AddMention(account, addText: false) - .Build(); - - Assert.Equal("original text", activity.Properties["text"]); - Assert.NotNull(activity.Entities); - Assert.Single(activity.Entities); - } - - [Fact] - public void AddMention_MultipleMentions_AddsAllMentions() - { - ConversationAccount account1 = new() { Id = "user-1", Name = "User One" }; - ConversationAccount account2 = new() { Id = "user-2", Name = "User Two" }; - - TeamsActivity activity = builder - .WithText("message") - .AddMention(account1) - .AddMention(account2) - .Build(); - - Assert.Equal("User Two User One message", activity.Properties["text"]); - Assert.NotNull(activity.Entities); - Assert.Equal(2, activity.Entities?.Count); - } - - [Fact] - public void FluentAPI_CompleteActivity_BuildsCorrectly() - { - TeamsActivity activity = builder - .WithType(TeamsActivityType.Message) - .WithId("activity-123") - .WithChannelId("msteams") - .WithText("Test message") - .WithServiceUrl(new Uri("https://smba.trafficmanager.net/teams/")) - .WithFrom(TeamsConversationAccount.FromConversationAccount(new ConversationAccount - { - Id = "sender-id", - Name = "Sender" - })) - .WithRecipient(TeamsConversationAccount.FromConversationAccount(new ConversationAccount - { - Id = "recipient-id", - Name = "Recipient" - })) - .WithConversation(TeamsConversation.FromConversation(new Conversation - { - Id = "conv-id" - })) - .AddEntity(new ClientInfoEntity { Locale = "en-US" }) - .AddAttachment(new TeamsAttachment { ContentType = "text/html" }) - .AddMention(new ConversationAccount { Id = "user-1", Name = "User" }) - .Build(); - - Assert.Equal(TeamsActivityType.Message, activity.Type); - Assert.Equal("activity-123", activity.Id); - Assert.Equal("msteams", activity.ChannelId); - Assert.Equal("User Test message", activity.Properties["text"]); - Assert.Equal("sender-id", activity.From?.Id); - Assert.Equal("recipient-id", activity.Recipient?.Id); - Assert.Equal("conv-id", activity.Conversation?.Id); - Assert.NotNull(activity.Entities); - Assert.Equal(2, activity.Entities?.Count); // ClientInfo + Mention - Assert.NotNull(activity.Attachments); - Assert.Single(activity.Attachments); - } - [Fact] public void FluentAPI_MethodChaining_ReturnsBuilderInstance() { - TeamsActivityBuilder result1 = builder.WithId("id"); - TeamsActivityBuilder result2 = builder.WithText("text"); - TeamsActivityBuilder result3 = builder.WithType(TeamsActivityType.Message); + TeamsActivityBuilder result2 = builder.WithType(TeamsActivityType.Message); Assert.Same(builder, result1); Assert.Same(builder, result2); - Assert.Same(builder, result3); } [Fact] public void Build_CalledMultipleTimes_ReturnsSameInstance() { - builder - .WithId("test-id"); + builder.WithId("test-id"); TeamsActivity activity1 = builder.Build(); TeamsActivity activity2 = builder.Build(); @@ -480,43 +317,6 @@ public void Build_CalledMultipleTimes_ReturnsSameInstance() Assert.Same(activity1, activity2); } - [Fact] - public void Builder_ModifyingExistingActivity_PreservesOriginalData() - { - TeamsActivity original = new() - { - Id = "original-id", - Type = TeamsActivityType.Message - }; - original.Properties["text"] = "original text"; - - TeamsActivity modified = TeamsActivity.CreateBuilder(original) - .WithText("modified text") - .Build(); - - Assert.Equal("original-id", modified.Id); - Assert.Equal("modified text", modified.Properties["text"]); - Assert.Equal(TeamsActivityType.Message, modified.Type); - } - - [Fact] - public void AddMention_UpdatesBaseEntityCollection() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = "Test User" - }; - - TeamsActivity activity = builder - .AddMention(account) - .Build(); - - CoreActivity baseActivity = activity; - Assert.NotNull(baseActivity.Entities); - Assert.NotEmpty(baseActivity.Entities); - } - [Fact] public void WithChannelData_NullValue_SetsToNull() { @@ -531,7 +331,6 @@ public void WithChannelData_NullValue_SetsToNull() public void AddEntity_NullEntitiesCollection_InitializesCollection() { TeamsActivity activity = builder.Build(); - Assert.Null(activity.Entities); ClientInfoEntity entity = new() { Locale = "en-US" }; @@ -546,7 +345,6 @@ public void AddEntity_NullEntitiesCollection_InitializesCollection() public void AddAttachment_NullAttachmentsCollection_InitializesCollection() { TeamsActivity activity = builder.Build(); - Assert.Null(activity.Attachments); TeamsAttachment attachment = new() { ContentType = "text/html" }; @@ -557,22 +355,6 @@ public void AddAttachment_NullAttachmentsCollection_InitializesCollection() Assert.Single(result.Attachments); } - [Fact] - public void Builder_EmptyText_AddMention_PrependsMention() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = "User" - }; - - TeamsActivity activity = builder - .AddMention(account) - .Build(); - - Assert.Equal("User ", activity.Properties["text"]); - } - [Fact] public void WithConversationReference_WithNullActivity_ThrowsArgumentNullException() { @@ -582,7 +364,6 @@ public void WithConversationReference_WithNullActivity_ThrowsArgumentNullExcepti [Fact] public void WithConversationReference_WithNullChannelId_ThrowsArgumentNullException() { - TeamsActivity sourceActivity = new() { ChannelId = null, @@ -623,7 +404,6 @@ public void WithConversationReference_WithEmptyConversationId_DoesNotThrow() }; TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); - Assert.NotNull(result.Conversation); } @@ -640,7 +420,6 @@ public void WithConversationReference_WithEmptyFromId_DoesNotThrow() }; TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); - Assert.NotNull(result.From); } @@ -657,7 +436,6 @@ public void WithConversationReference_WithEmptyRecipientId_DoesNotThrow() }; TeamsActivity result = builder.WithConversationReference(sourceActivity).Build(); - Assert.NotNull(result.From); } @@ -735,30 +513,10 @@ public void WithAttachments_WithNullValue_SetsToNull() Assert.Null(activity.Attachments); } - [Fact] - public void AddMention_WithAccountWithNullName_UsesNullText() - { - ConversationAccount account = new() - { - Id = "user-123", - Name = null - }; - - TeamsActivity activity = builder - .WithText("message") - .AddMention(account) - .Build(); - - Assert.Equal(" message", activity.Properties["text"]); - Assert.NotNull(activity.Entities); - Assert.Single(activity.Entities); - } - [Fact] public void Build_MultipleCalls_ReturnsRebasedActivity() { - builder - .AddEntity(new ClientInfoEntity { Locale = "en-US" }); + builder.AddEntity(new ClientInfoEntity { Locale = "en-US" }); TeamsActivity activity1 = builder.Build(); CoreActivity baseActivity1 = activity1; @@ -772,82 +530,4 @@ public void Build_MultipleCalls_ReturnsRebasedActivity() Assert.NotNull(baseActivity2.Entities); Assert.Equal(2, activity2.Entities!.Count); } - - [Fact] - public void IntegrationTest_CreateComplexActivity() - { - Uri serviceUrl = new("https://smba.trafficmanager.net/amer/test/"); - TeamsChannelData channelData = new() - { - TeamsChannelId = "19:channel@thread.tacv2", - TeamsTeamId = "19:team@thread.tacv2" - }; - - Conversation conv = new() - { - Id = "conv-001", - Properties = - { - { "tenantId", "tenant-001" }, - { "conversationType", "channel" } - } - }; - - TeamsConversation? tc = TeamsConversation.FromConversation(conv); - Assert.NotNull(tc); - - TeamsActivity activity = builder - .WithType(TeamsActivityType.Message) - .WithId("msg-001") - .WithServiceUrl(serviceUrl) - .WithChannelId("msteams") - .WithText("Please review this document") - .WithFrom(TeamsConversationAccount.FromConversationAccount(new ConversationAccount - { - Id = "bot-id", - Name = "Bot" - })) - .WithRecipient(TeamsConversationAccount.FromConversationAccount(new ConversationAccount - { - Id = "user-id", - Name = "User" - })) - .WithConversation(tc) - .WithChannelData(channelData) - .AddEntity(new ClientInfoEntity - { - Locale = "en-US", - Country = "US", - Platform = "Web" - }) - .AddAttachment(new TeamsAttachment - { - ContentType = "application/vnd.microsoft.card.adaptive", - Name = "adaptive-card.json" - }) - .AddMention(new ConversationAccount - { - Id = "manager-id", - Name = "Manager" - }, "Manager") - .Build(); - - // Verify all properties - Assert.Equal(TeamsActivityType.Message, activity.Type); - Assert.Equal("msg-001", activity.Id); - Assert.Equal(serviceUrl, activity.ServiceUrl); - Assert.Equal("msteams", activity.ChannelId); - Assert.Equal("Manager Please review this document", activity.Properties["text"]); - Assert.Equal("bot-id", activity.From?.Id); - Assert.Equal("user-id", activity.Recipient?.Id); - Assert.Equal("conv-001", activity.Conversation?.Id); - Assert.Equal("tenant-001", activity.Conversation?.TenantId); - Assert.Equal("channel", activity.Conversation?.ConversationType); - Assert.NotNull(activity.ChannelData); - Assert.Equal("19:channel@thread.tacv2", activity.ChannelData?.TeamsChannelId); - Assert.NotNull(activity.Entities); - Assert.Equal(2, activity.Entities?.Count); // ClientInfo + Mention - Assert.NotNull(activity.Attachments); - Assert.Single(activity.Attachments); - } } diff --git a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs index 85a461f05..193db8094 100644 --- a/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs +++ b/core/test/Microsoft.Teams.Bot.Apps.UnitTests/TeamsActivityTests.cs @@ -121,8 +121,7 @@ static void SerializeAndAssert(CoreActivity a) [Fact] public void TeamsActivityBuilder_FluentAPI() { - TeamsActivity activity = TeamsActivity.CreateBuilder() - .WithType(TeamsActivityType.Message) + MessageActivity activity = MessageActivity.CreateBuilder() .WithText("Hello World") .WithChannelId("msteams") .AddMention(new ConversationAccount @@ -133,7 +132,7 @@ public void TeamsActivityBuilder_FluentAPI() .Build(); Assert.Equal(ActivityType.Message, activity.Type); - Assert.Equal("TestUser Hello World", activity.Properties["text"]); + Assert.Equal("TestUser Hello World", activity.Text); Assert.Equal("msteams", activity.ChannelId); Assert.NotNull(activity.Entities); Assert.Single(activity.Entities); @@ -147,8 +146,7 @@ public void TeamsActivityBuilder_FluentAPI() [Fact] public void Serialize_TeamsActivity_WithEntities() { - TeamsActivity activity = TeamsActivity.CreateBuilder() - .WithType(ActivityType.Message) + MessageActivity activity = MessageActivity.CreateBuilder() .WithText("Hello World") .WithChannelId("msteams") .Build();