diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs new file mode 100644 index 00000000..9be3d6bf --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Activity.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Represents an Agent 365 agentLifecycle event activity. +/// +public class AgentLifecycleEventActivity : EventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgentLifecycleEventActivity() : base() + { + Name = EventNames.AgentLifecycle; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgentLifecycleEventActivity(EventActivity activity) : base(activity) + { + Name ??= EventNames.AgentLifecycle; + } + + /// + /// Creates the most-specific Agent 365 lifecycle activity type for the supplied event activity. + /// + /// The source event activity. + /// A typed lifecycle event activity when the valueType is known; otherwise a base lifecycle activity. + internal static AgentLifecycleEventActivity FromEventActivity(EventActivity activity) + { + ArgumentNullException.ThrowIfNull(activity); + + return activity.ValueType switch + { + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated => new AgenticUserIdentityCreatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated => new AgenticUserIdentityUpdatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated => new AgenticUserManagerUpdatedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserEnabled => new AgenticUserEnabledActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserDisabled => new AgenticUserDisabledActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserDeleted => new AgenticUserDeletedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserUndeleted => new AgenticUserUndeletedActivity(activity), + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated => new AgenticUserWorkloadOnboardingUpdatedActivity(activity), + _ => new AgentLifecycleEventActivity(activity), + }; + } +} + +/// +/// Represents an Agent 365 agentLifecycle event activity with a strongly-typed value payload. +/// +/// The lifecycle event value payload type. +public class AgentLifecycleEventActivity : AgentLifecycleEventActivity +{ + /// + /// Gets or sets the strongly-typed value associated with the lifecycle event activity. + /// + [JsonPropertyName("value")] + public new TValue? Value + { + get => base.Value != null ? JsonSerializer.Deserialize(base.Value.ToJsonString()) : default; + set => base.Value = value != null ? JsonSerializer.SerializeToNode(value) : null; + } + + /// + /// Initializes a new instance of the class. + /// + internal AgentLifecycleEventActivity() : base() + { + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgentLifecycleEventActivity(EventActivity activity) : base(activity) + { + } +} + +/// +/// Fired when an agentic user identity is created. +/// +public class AgenticUserIdentityCreatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserIdentityCreatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserIdentityCreated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserIdentityCreatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserIdentityCreated; + } +} + +/// +/// Fired when an agentic user identity property changes. +/// +public class AgenticUserIdentityUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserIdentityUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserIdentityUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated; + } +} + +/// +/// Fired when an agentic user's manager changes. +/// +public class AgenticUserManagerUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserManagerUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserManagerUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserManagerUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserManagerUpdated; + } +} + +/// +/// Fired when an agentic user is enabled. +/// +public class AgenticUserEnabledActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserEnabledActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserEnabled; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserEnabledActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserEnabled; + } +} + +/// +/// Fired when an agentic user is disabled. +/// +public class AgenticUserDisabledActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserDisabledActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserDisabled; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserDisabledActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserDisabled; + } +} + +/// +/// Fired when an agentic user is deleted. +/// +public class AgenticUserDeletedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserDeletedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserDeleted; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserDeletedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserDeleted; + } +} + +/// +/// Fired when a previously deleted agentic user is restored. +/// +public class AgenticUserUndeletedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserUndeletedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserUndeleted; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserUndeletedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserUndeleted; + } +} + +/// +/// Fired when a workload onboarding state changes for an agentic user. +/// +public class AgenticUserWorkloadOnboardingUpdatedActivity : AgentLifecycleEventActivity +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal AgenticUserWorkloadOnboardingUpdatedActivity() : base() + { + ValueType = AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated; + } + + /// + /// Initializes a new instance of the class from an event activity. + /// + /// The source event activity. + internal AgenticUserWorkloadOnboardingUpdatedActivity(EventActivity activity) : base(activity) + { + ValueType ??= AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated; + } +} + +/// +/// String constants for Agent 365 lifecycle activity valueType values. +/// +public static class AgentLifecycleEventValueTypes +{ + /// Agentic user identity created value type. + public const string AgenticUserIdentityCreated = "AgenticUserIdentityCreated"; + + /// Agentic user identity updated value type. + public const string AgenticUserIdentityUpdated = "AgenticUserIdentityUpdated"; + + /// Agentic user manager updated value type. + public const string AgenticUserManagerUpdated = "AgenticUserManagerUpdated"; + + /// Agentic user enabled value type. + public const string AgenticUserEnabled = "AgenticUserEnabled"; + + /// Agentic user disabled value type. + public const string AgenticUserDisabled = "AgenticUserDisabled"; + + /// Agentic user deleted value type. + public const string AgenticUserDeleted = "AgenticUserDeleted"; + + /// Agentic user undeleted value type. + public const string AgenticUserUndeleted = "AgenticUserUndeleted"; + + /// Agentic user workload onboarding updated value type. + public const string AgenticUserWorkloadOnboardingUpdated = "AgenticUserWorkloadOnboardingUpdated"; +} + +/// +/// String constants for Agent 365 lifecycle payload eventType values. +/// +public static class AgentLifecycleEventTypes +{ + /// Agentic user identity created event type. + public const string AgenticUserIdentityCreated = "agenticUserIdentityCreated"; + + /// Agentic user identity updated event type. + public const string AgenticUserIdentityUpdated = "agenticUserIdentityUpdated"; + + /// Agentic user manager updated event type. + public const string AgenticUserManagerUpdated = "agenticUserManagerUpdated"; + + /// Agentic user enabled event type. + public const string AgenticUserEnabled = "agenticUserEnabled"; + + /// Agentic user disabled event type. + public const string AgenticUserDisabled = "agenticUserDisabled"; + + /// Agentic user deleted event type. + public const string AgenticUserDeleted = "agenticUserDeleted"; + + /// Agentic user undeleted event type. + public const string AgenticUserUndeleted = "agenticUserUndeleted"; + + /// Agentic user workload onboarding updated event type. + public const string AgenticUserWorkloadOnboardingUpdated = "agenticUserWorkloadOnboardingUpdated"; +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs new file mode 100644 index 00000000..8838c20b --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.Values.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Manager profile carried by the AgenticUserIdentityCreated lifecycle event. +/// +public class AgentLifecycleManager +{ + /// + /// Gets or sets the Entra object ID of the manager. + /// + [JsonPropertyName("userId")] + public string? UserId { get; set; } + + /// + /// Gets or sets the manager's email address. + /// + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// + /// Gets or sets the manager's display name. + /// + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } +} + +/// +/// Manager reference carried by the AgenticUserManagerUpdated lifecycle event. +/// +public class AgentLifecycleManagerRef +{ + /// + /// Gets or sets the Entra object ID of the manager. + /// + [JsonPropertyName("managerId")] + public string? ManagerId { get; set; } +} + +/// +/// A single property change carried by the AgenticUserIdentityUpdated lifecycle event. +/// +public class AgentLifecycleUpdatedProperty +{ + /// + /// Gets or sets the name of the property that changed. + /// + [JsonPropertyName("propertyName")] + public required string PropertyName { get; set; } + + /// + /// Gets or sets the new value of the property. + /// + [JsonPropertyName("propertyValue")] + public string? PropertyValue { get; set; } +} + +/// +/// Fields shared by every Agent 365 lifecycle event payload. +/// +public abstract class AgentLifecycleValueBase +{ + /// + /// Gets or sets the tenant that the agentic user belongs to. + /// + [JsonPropertyName("tenantId")] + public string? TenantId { get; set; } + + /// + /// Gets or sets the Agent ID user-shaped identity object ID. + /// + [JsonPropertyName("agenticUserId")] + public string? AgenticUserId { get; set; } + + /// + /// Gets or sets the concrete agent app instance ID. + /// + [JsonPropertyName("agenticAppInstanceId")] + public string? AgenticAppInstanceId { get; set; } + + /// + /// Gets or sets the agent identity blueprint app ID. + /// + [JsonPropertyName("agentIdentityBlueprintId")] + public string? AgentIdentityBlueprintId { get; set; } + + /// + /// Gets or sets the monotonic version of the agentic user state when provided by the service. + /// + [JsonPropertyName("version")] + public int? Version { get; set; } +} + +/// +/// Payload for the AgenticUserIdentityCreated lifecycle event. +/// +public class AgenticUserIdentityCreatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserIdentityCreated; + + /// + /// Gets or sets the manager assigned to the agentic user at creation. + /// + [JsonPropertyName("manager")] + public AgentLifecycleManager? Manager { get; set; } + + /// + /// Gets or sets when the agentic user identity expires. + /// + [JsonPropertyName("expirationDateTime")] + public DateTimeOffset? ExpirationDateTime { get; set; } +} + +/// +/// Payload for the AgenticUserIdentityUpdated lifecycle event. +/// +public class AgenticUserIdentityUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserIdentityUpdated; + + /// + /// Gets or sets the property that changed. + /// + [JsonPropertyName("updatedProperty")] + public required AgentLifecycleUpdatedProperty UpdatedProperty { get; set; } +} + +/// +/// Payload for the AgenticUserManagerUpdated lifecycle event. +/// +public class AgenticUserManagerUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserManagerUpdated; + + /// + /// Gets or sets the new manager reference. The value is absent when the manager was removed. + /// + [JsonPropertyName("manager")] + public AgentLifecycleManagerRef? Manager { get; set; } +} + +/// +/// Payload for the AgenticUserEnabled lifecycle event. +/// +public class AgenticUserEnabledValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserEnabled; +} + +/// +/// Payload for the AgenticUserDisabled lifecycle event. +/// +public class AgenticUserDisabledValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserDisabled; +} + +/// +/// Payload for the AgenticUserDeleted lifecycle event. +/// +public class AgenticUserDeletedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserDeleted; + + /// + /// Gets or sets the reason the agentic user was deleted. + /// + [JsonPropertyName("deletionReason")] + public string? DeletionReason { get; set; } +} + +/// +/// Payload for the AgenticUserUndeleted lifecycle event. +/// +public class AgenticUserUndeletedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserUndeleted; +} + +/// +/// Payload for the AgenticUserWorkloadOnboardingUpdated lifecycle event. +/// +public class AgenticUserWorkloadOnboardingUpdatedValue : AgentLifecycleValueBase +{ + /// + /// Gets or sets the lifecycle event type. + /// + [JsonPropertyName("eventType")] + public string EventType { get; set; } = AgentLifecycleEventTypes.AgenticUserWorkloadOnboardingUpdated; + + /// + /// Gets or sets the workload being onboarded. + /// + [JsonPropertyName("workloadName")] + public string? WorkloadName { get; set; } + + /// + /// Gets or sets the onboarding state for the workload. + /// + [JsonPropertyName("workloadOnboardingState")] + public string? WorkloadOnboardingState { get; set; } +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs new file mode 100644 index 00000000..7c0ec862 --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Handlers/AgentLifecycleHandler.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Apps.Routing; +using Microsoft.Teams.Apps.Schema; + +namespace Microsoft.Teams.Apps.Handlers; + +/// +/// Delegate for handling all Agent 365 lifecycle event activity variants. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgentLifecycleHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user identity created lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserIdentityCreatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user identity updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserIdentityUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user manager updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserManagerUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user enabled lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserEnabledHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user disabled lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserDisabledHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user deleted lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserDeletedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user undeleted lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserUndeletedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Delegate for handling agentic user workload onboarding updated lifecycle event activities. +/// +/// The context for the lifecycle event activity. +/// A cancellation token that can be used to cancel the operation. +/// A task representing the asynchronous operation. +public delegate Task AgenticUserWorkloadOnboardingUpdatedHandler(Context context, CancellationToken cancellationToken = default); + +/// +/// Extension methods for registering Agent 365 lifecycle event activity handlers. +/// +public static class AgentLifecycleExtensions +{ + /// + /// Registers a handler for all Agent 365 agentLifecycle event activity variants. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgentLifecycle(this TeamsBotApplication app, AgentLifecycleHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + EventNames.AgentLifecycle, + valueType: null, + AgentLifecycleEventActivity.FromEventActivity, + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserIdentityCreated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserIdentityCreated(this TeamsBotApplication app, AgenticUserIdentityCreatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, + activity => new AgenticUserIdentityCreatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserIdentityUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserIdentityUpdated(this TeamsBotApplication app, AgenticUserIdentityUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, + activity => new AgenticUserIdentityUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserManagerUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserManagerUpdated(this TeamsBotApplication app, AgenticUserManagerUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, + activity => new AgenticUserManagerUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserEnabled lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserEnabled(this TeamsBotApplication app, AgenticUserEnabledHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserEnabled, + AgentLifecycleEventValueTypes.AgenticUserEnabled, + activity => new AgenticUserEnabledActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserDisabled lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserDisabled(this TeamsBotApplication app, AgenticUserDisabledHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserDisabled, + AgentLifecycleEventValueTypes.AgenticUserDisabled, + activity => new AgenticUserDisabledActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserDeleted lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserDeleted(this TeamsBotApplication app, AgenticUserDeletedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserDeleted, + AgentLifecycleEventValueTypes.AgenticUserDeleted, + activity => new AgenticUserDeletedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserUndeleted lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserUndeleted(this TeamsBotApplication app, AgenticUserUndeletedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserUndeleted, + AgentLifecycleEventValueTypes.AgenticUserUndeleted, + activity => new AgenticUserUndeletedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + /// + /// Registers a handler for AgenticUserWorkloadOnboardingUpdated lifecycle event activities. + /// + /// The Teams bot application. + /// The handler to register. + /// The updated Teams bot application. + public static TeamsBotApplication OnAgenticUserWorkloadOnboardingUpdated(this TeamsBotApplication app, AgenticUserWorkloadOnboardingUpdatedHandler handler) + { + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return app.RegisterAgentLifecycleRoute( + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, + activity => new AgenticUserWorkloadOnboardingUpdatedActivity(activity), + (ctx, cancellationToken) => handler(ctx, cancellationToken)); + } + + private static TeamsBotApplication RegisterAgentLifecycleRoute( + this TeamsBotApplication app, + string routeSuffix, + string? valueType, + Func createActivity, + Func, CancellationToken, Task> handler) where TActivity : AgentLifecycleEventActivity + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentException.ThrowIfNullOrWhiteSpace(routeSuffix); + ArgumentNullException.ThrowIfNull(createActivity, nameof(createActivity)); + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + + string routeName = valueType is null + ? string.Join("/", TeamsActivityTypes.Event, routeSuffix) + : string.Join("/", TeamsActivityTypes.Event, EventNames.AgentLifecycle, routeSuffix); + + app.Router.Register(new Route + { + Name = routeName, + Selector = activity => + activity.Name == EventNames.AgentLifecycle + && (valueType is null || activity.ValueType == valueType), + Handler = async (ctx, cancellationToken) => + { + TActivity typedActivity = createActivity(ctx.Activity); + var typedContext = ctx.CreateDerivedContext(typedActivity); + await handler(typedContext, cancellationToken).ConfigureAwait(false); + } + }); + + return app; + } +} diff --git a/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs b/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs index c06830ce..ad792020 100644 --- a/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs +++ b/core/src/Microsoft.Teams.Apps/Handlers/EventHandler.Activity.cs @@ -20,7 +20,10 @@ public class EventActivity : TeamsActivity public static new EventActivity FromActivity(CoreActivity activity) { ArgumentNullException.ThrowIfNull(activity); - return new EventActivity(activity); + EventActivity eventActivity = new(activity); + return eventActivity.Name == EventNames.AgentLifecycle + ? AgentLifecycleEventActivity.FromEventActivity(eventActivity) + : eventActivity; } /// @@ -29,12 +32,21 @@ public class EventActivity : TeamsActivity [JsonPropertyName("name")] public string? Name { get; set; } + /// + /// Gets or sets the value payload type for event activities that carry typed variants. + /// + [JsonPropertyName("valueType")] + public string? ValueType { get; set; } + /// /// Gets or sets the value payload of the event activity. /// [JsonPropertyName("value")] public JsonNode? Value { get; set; } + /// + public override string ToJson() => CoreActivity.ToJson(this); + /// /// Initializes a new instance of the class. /// @@ -48,10 +60,17 @@ internal EventActivity() : base(TeamsActivityTypes.Event) /// internal EventActivity(CoreActivity activity) : base(activity) { - Name = activity.Properties.Extract("name"); - Value = activity is EventActivity evt - ? evt.Value - : activity.Properties.Extract("value"); + if (activity is EventActivity evt) + { + Name = evt.Name; + ValueType = evt.ValueType; + Value = evt.Value; + return; + } + + Name = Properties.Extract("name"); + ValueType = Properties.Extract("valueType"); + Value = Properties.Extract("value"); } } @@ -91,6 +110,9 @@ internal EventActivity(EventActivity activity) : base(activity) /// public static class EventNames { + /// Agent 365 lifecycle event name. + public const string AgentLifecycle = "agentLifecycle"; + /// Meeting start event name. public const string MeetingStart = "application/vnd.microsoft.meetingStart"; diff --git a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs index 54347ad6..4deb08d3 100644 --- a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs +++ b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivity.cs @@ -68,8 +68,8 @@ internal TeamsActivity(CoreActivity activity) : base(activity) base.From = TeamsChannelAccount.FromChannelAccount(activity.From) ?? new TeamsChannelAccount(); base.Recipient = TeamsChannelAccount.FromChannelAccount(activity.Recipient) ?? new TeamsChannelAccount(); base.Conversation = TeamsConversation.FromConversation(activity.Conversation) ?? new TeamsConversation(); - ChannelData = activity.Properties.Extract("channelData"); - Entities = activity.Properties.Extract("entities"); + ChannelData = Properties.Extract("channelData"); + Entities = Properties.Extract("entities"); } /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs new file mode 100644 index 00000000..3b8dc46d --- /dev/null +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgentLifecycleHandlerTests.cs @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Teams.Apps.Api.Clients; +using Microsoft.Teams.Apps.Handlers; +using Microsoft.Teams.Apps.Schema; +using Microsoft.Teams.Apps.State; +using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Schema; +using Moq; + +namespace Microsoft.Teams.Apps.UnitTests; + +public class AgentLifecycleHandlerTests +{ + private const string TenantId = "00000000-0000-0000-0000-000000000001"; + private const string AgenticUserId = "00000000-0000-0000-0000-000000000002"; + private const string AppId = "00000000-0000-0000-0000-000000000003"; + private const string AgenticAppInstanceId = "00000000-0000-0000-0000-000000000004"; + private const string BlueprintId = "00000000-0000-0000-0000-000000000005"; + private const string ManagerId = "3c22b565-74f3-48b0-aa18-1dc03b8ec270"; + + public static TheoryData VariantTypes => new() + { + { AgentLifecycleEventValueTypes.AgenticUserIdentityCreated, typeof(AgenticUserIdentityCreatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated, typeof(AgenticUserIdentityUpdatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, typeof(AgenticUserManagerUpdatedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserEnabled, typeof(AgenticUserEnabledActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserDisabled, typeof(AgenticUserDisabledActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserDeleted, typeof(AgenticUserDeletedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserUndeleted, typeof(AgenticUserUndeletedActivity) }, + { AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated, typeof(AgenticUserWorkloadOnboardingUpdatedActivity) }, + }; + + [Theory] + [MemberData(nameof(VariantTypes))] + public void FromActivity_DiscriminatesAgentLifecycleVariants(string valueType, Type expectedType) + { + TeamsActivity activity = ParseLifecycleActivity(valueType); + + Assert.Equal(expectedType, activity.GetType()); + + EventActivity eventActivity = Assert.IsAssignableFrom(activity); + Assert.Equal(TeamsActivityTypes.Event, eventActivity.Type); + Assert.Equal(EventNames.AgentLifecycle, eventActivity.Name); + Assert.Equal(valueType, eventActivity.ValueType); + AssertLifecycleValue(activity, valueType); + } + + [Fact] + public void FromActivity_KeepsUnknownAgentLifecycleValueTypeAsBaseLifecycleActivity() + { + TeamsActivity activity = ParseLifecycleActivity("UnknownLifecycleValueType", CommonValue("unknownLifecycleEvent")); + + AgentLifecycleEventActivity lifecycleActivity = Assert.IsType(activity); + Assert.Equal(EventNames.AgentLifecycle, lifecycleActivity.Name); + Assert.Equal("UnknownLifecycleValueType", lifecycleActivity.ValueType); + Assert.NotNull(lifecycleActivity.Value); + } + + [Fact] + public void ToJson_PreservesAgentLifecycleEnvelope() + { + AgenticUserManagerUpdatedActivity activity = Assert.IsType( + ParseLifecycleActivity(AgentLifecycleEventValueTypes.AgenticUserManagerUpdated)); + + string json = activity.ToJson(); + using JsonDocument document = JsonDocument.Parse(json); + JsonElement root = document.RootElement; + Assert.Equal(1, CountOccurrences(json, "\"valueType\"")); + Assert.Equal(1, CountOccurrences(json, "\"valueType\"")); + Assert.Equal(TeamsActivityTypes.Event, root.GetProperty("type").GetString()); + Assert.Equal(EventNames.AgentLifecycle, root.GetProperty("name").GetString()); + Assert.Equal(AgentLifecycleEventValueTypes.AgenticUserManagerUpdated, root.GetProperty("valueType").GetString()); + Assert.Equal(AgenticAppInstanceId, root.GetProperty("value").GetProperty("agenticAppInstanceId").GetString()); + Assert.Equal(ManagerId, root.GetProperty("value").GetProperty("manager").GetProperty("managerId").GetString()); + } + + [Fact] + public async Task OnEvent_StillReceivesAgentLifecycleEvents() + { + TeamsBotApplication app = CreateApp(); + bool called = false; + + app.OnEvent((ctx, _) => + { + called = true; + Assert.IsType(ctx.Activity); + Assert.Equal(EventNames.AgentLifecycle, ctx.Activity.Name); + Assert.Equal(AgentLifecycleEventValueTypes.AgenticUserEnabled, ctx.Activity.ValueType); + return Task.CompletedTask; + }); + + await DispatchAsync(app, AgentLifecycleEventValueTypes.AgenticUserEnabled); + + Assert.True(called); + } + + [Theory] + [MemberData(nameof(VariantTypes))] + public async Task VariantHandlers_DispatchTypedContextAndPropagateState(string valueType, Type expectedType) + { + TeamsBotApplication app = CreateApp(); + TurnStateContainer state = CreateState(); + int lifecycleHandlerCalls = 0; + int variantHandlerCalls = 0; + + app.OnAgentLifecycle((ctx, _) => + { + lifecycleHandlerCalls++; + Assert.Same(state, ctx.State); + Assert.Equal(valueType, ctx.Activity.ValueType); + Assert.Equal(expectedType, AgentLifecycleEventActivity.FromEventActivity(ctx.Activity).GetType()); + return Task.CompletedTask; + }); + + RegisterVariantHandler(app, valueType, expectedType, state, () => variantHandlerCalls++); + + await DispatchAsync(app, valueType, state); + + Assert.Equal(1, lifecycleHandlerCalls); + Assert.Equal(1, variantHandlerCalls); + } + + [Fact] + public async Task VariantHandler_DoesNotDispatchForDifferentValueType() + { + TeamsBotApplication app = CreateApp(); + bool called = false; + + app.OnAgenticUserEnabled((_, _) => + { + called = true; + return Task.CompletedTask; + }); + + await DispatchAsync(app, AgentLifecycleEventValueTypes.AgenticUserDisabled); + + Assert.False(called); + } + + private static void RegisterVariantHandler( + TeamsBotApplication app, + string valueType, + Type expectedType, + TurnStateContainer state, + Action onCalled) + { + switch (valueType) + { + case AgentLifecycleEventValueTypes.AgenticUserIdentityCreated: + app.OnAgenticUserIdentityCreated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(ManagerId, ctx.Activity.Value?.Manager?.UserId); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated: + app.OnAgenticUserIdentityUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("Mail", ctx.Activity.Value?.UpdatedProperty.PropertyName); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserManagerUpdated: + app.OnAgenticUserManagerUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(ManagerId, ctx.Activity.Value?.Manager?.ManagerId); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserEnabled: + app.OnAgenticUserEnabled((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(6, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDisabled: + app.OnAgenticUserDisabled((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(7, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDeleted: + app.OnAgenticUserDeleted((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("UserSoftDelete", ctx.Activity.Value?.DeletionReason); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserUndeleted: + app.OnAgenticUserUndeleted((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal(9, ctx.Activity.Value?.Version); + return Task.CompletedTask; + }); + break; + + case AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated: + app.OnAgenticUserWorkloadOnboardingUpdated((ctx, _) => + { + AssertVariantContext(ctx, expectedType, valueType, state, onCalled); + Assert.Equal("Teams", ctx.Activity.Value?.WorkloadName); + Assert.Equal("succeeded", ctx.Activity.Value?.WorkloadOnboardingState); + return Task.CompletedTask; + }); + break; + } + } + + private static void AssertVariantContext( + Context context, + Type expectedType, + string valueType, + TurnStateContainer state, + Action onCalled) where TActivity : AgentLifecycleEventActivity + { + onCalled(); + Assert.Same(state, context.State); + Assert.Equal(expectedType, context.Activity.GetType()); + Assert.Equal(EventNames.AgentLifecycle, context.Activity.Name); + Assert.Equal(valueType, context.Activity.ValueType); + } + + private static void AssertLifecycleValue(TeamsActivity activity, string valueType) + { + switch (valueType) + { + case AgentLifecycleEventValueTypes.AgenticUserIdentityCreated: + AgenticUserIdentityCreatedActivity created = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserIdentityCreated, created.Value?.EventType); + Assert.Equal(AgenticUserId, created.Value?.AgenticUserId); + Assert.Equal(ManagerId, created.Value?.Manager?.UserId); + Assert.Equal("manager@example.test", created.Value?.Manager?.Email); + Assert.Equal(1, created.Value?.ExpirationDateTime?.Year); + break; + + case AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated: + AgenticUserIdentityUpdatedActivity updated = Assert.IsType(activity); + Assert.Equal("Mail", updated.Value?.UpdatedProperty.PropertyName); + Assert.Equal("newinstance4@teamssdk.onmicrosoft.com", updated.Value?.UpdatedProperty.PropertyValue); + Assert.Equal(4, updated.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserManagerUpdated: + AgenticUserManagerUpdatedActivity managerUpdated = Assert.IsType(activity); + Assert.Equal(ManagerId, managerUpdated.Value?.Manager?.ManagerId); + Assert.Equal(6, managerUpdated.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserEnabled: + AgenticUserEnabledActivity enabled = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserEnabled, enabled.Value?.EventType); + Assert.Equal(6, enabled.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDisabled: + AgenticUserDisabledActivity disabled = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserDisabled, disabled.Value?.EventType); + Assert.Equal(7, disabled.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserDeleted: + AgenticUserDeletedActivity deleted = Assert.IsType(activity); + Assert.Equal("UserSoftDelete", deleted.Value?.DeletionReason); + Assert.Equal(8, deleted.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserUndeleted: + AgenticUserUndeletedActivity undeleted = Assert.IsType(activity); + Assert.Equal(AgentLifecycleEventTypes.AgenticUserUndeleted, undeleted.Value?.EventType); + Assert.Equal(9, undeleted.Value?.Version); + break; + + case AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated: + AgenticUserWorkloadOnboardingUpdatedActivity workload = Assert.IsType(activity); + Assert.Equal("Teams", workload.Value?.WorkloadName); + Assert.Equal("succeeded", workload.Value?.WorkloadOnboardingState); + break; + } + } + + private static Task DispatchAsync(TeamsBotApplication app, string valueType, TurnStateContainer? state = null) + { + EventActivity activity = Assert.IsAssignableFrom(ParseLifecycleActivity(valueType)); + Context context = new(app, activity); + if (state is not null) + { + context.State = state; + } + + return app.Router.DispatchAsync(context); + } + + private static TeamsActivity ParseLifecycleActivity(string valueType) + => ParseLifecycleActivity(valueType, ValueFor(valueType)); + + private static TeamsActivity ParseLifecycleActivity(string valueType, string valueJson) + { + string json = $$""" + { + "recipient": { + "agenticUserId": "{{AgenticUserId}}", + "agenticAppId": "{{AppId}}", + "agenticAppBlueprintId": "{{BlueprintId}}", + "callbackUri": "https://example.test/api/messages", + "tenantId": "{{TenantId}}", + "role": "agenticUser", + "id": "{{AgenticUserId}}" + }, + "type": "event", + "id": "activity-id", + "timestamp": "2026-06-29T00:00:00Z", + "serviceUrl": "https://smba.trafficmanager.net/amer/tenant/", + "channelId": "agents", + "from": { "id": "system", "name": "System", "tenantId": "{{TenantId}}" }, + "conversation": { "tenantId": "{{TenantId}}", "id": "conversation-id", "topic": null }, + "channelData": { "tenant": { "id": "{{TenantId}}" }, "productContext": null }, + "valueType": "{{valueType}}", + "value": {{valueJson}}, + "name": "agentLifecycle" + } + """; + + return TeamsActivity.FromActivity(CoreActivity.FromJsonString(json)); + } + + private static string ValueFor(string valueType) + => valueType switch + { + AgentLifecycleEventValueTypes.AgenticUserIdentityCreated => CommonValue( + AgentLifecycleEventTypes.AgenticUserIdentityCreated, + $$""" + "expirationDateTime": "0001-01-01T00:00:00+00:00", + "manager": { + "displayName": null, + "userId": "{{ManagerId}}", + "email": "manager@example.test" + } + """), + AgentLifecycleEventValueTypes.AgenticUserIdentityUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserIdentityUpdated, + """ + "updatedProperty": { + "propertyName": "Mail", + "propertyValue": "newinstance4@teamssdk.onmicrosoft.com" + }, + "version": 4 + """), + AgentLifecycleEventValueTypes.AgenticUserManagerUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserManagerUpdated, + $$""" + "manager": { "managerId": "{{ManagerId}}" }, + "version": 6 + """), + AgentLifecycleEventValueTypes.AgenticUserEnabled => CommonValue( + AgentLifecycleEventTypes.AgenticUserEnabled, + """ + "version": 6 + """), + AgentLifecycleEventValueTypes.AgenticUserDisabled => CommonValue( + AgentLifecycleEventTypes.AgenticUserDisabled, + """ + "version": 7 + """), + AgentLifecycleEventValueTypes.AgenticUserDeleted => CommonValue( + AgentLifecycleEventTypes.AgenticUserDeleted, + """ + "deletionReason": "UserSoftDelete", + "version": 8 + """), + AgentLifecycleEventValueTypes.AgenticUserUndeleted => CommonValue( + AgentLifecycleEventTypes.AgenticUserUndeleted, + """ + "version": 9 + """), + AgentLifecycleEventValueTypes.AgenticUserWorkloadOnboardingUpdated => CommonValue( + AgentLifecycleEventTypes.AgenticUserWorkloadOnboardingUpdated, + """ + "workloadName": "Teams", + "workloadOnboardingState": "succeeded" + """), + _ => CommonValue("unknownLifecycleEvent"), + }; + + private static string CommonValue(string eventType, string additionalJson = "") + { + string separator = string.IsNullOrWhiteSpace(additionalJson) ? string.Empty : "," + Environment.NewLine + additionalJson; + return $$""" + { + "tenantId": "{{TenantId}}", + "agenticUserId": "{{AgenticUserId}}", + "agenticAppInstanceId": "{{AgenticAppInstanceId}}", + "agentIdentityBlueprintId": "{{BlueprintId}}", + "eventType": "{{eventType}}"{{separator}} + } + """; + } + + private static int CountOccurrences(string value, string match) + { + int count = 0; + int index = 0; + while ((index = value.IndexOf(match, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += match.Length; + } + + return count; + } + + private static TurnStateContainer CreateState() + { + TurnState conversationState = new(); + conversationState.Set("test-key", "test-value"); + return new TurnStateContainer(conversationState, new TurnState()); + } + + private static TeamsBotApplication CreateApp() + { + Mock mockUserTokenClient = new( + new HttpClient(), + new Mock().Object, + NullLogger.Instance); + + Mock mockConversationClient = new( + new HttpClient(), + NullLogger.Instance); + + ApiClient apiClient = new( + new HttpClient(), + mockConversationClient.Object, + mockUserTokenClient.Object); + + return new TeamsBotApplication( + apiClient, + new HttpContextAccessor(), + NullLogger.Instance, + new TeamsBotApplicationOptions { AppId = "test-app-id" }); + } +}