From b2e15278ed97ead7e83aae251067f88ad761f5c3 Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 13:40:56 +0530 Subject: [PATCH 01/13] resilience management get tools --- Directory.Packages.props | 1 + Microsoft.Mcp.slnx | 7 + servers/Azure.Mcp.Server/src/Program.cs | 1 + .../src/AssemblyInfo.cs | 6 + ...zure.Mcp.Tools.ResilienceManagement.csproj | 19 + .../src/Commands/Drills/DrillGetCommand.cs | 93 +++ .../Resources/DrillResourceGetCommand.cs | 95 +++ .../Drills/Runs/DrillRunGetCommand.cs | 94 +++ .../Resources/DrillRunResourceGetCommand.cs | 97 +++ .../Assignments/GoalAssignmentGetCommand.cs | 92 +++ .../Goals/Resources/GoalResourceGetCommand.cs | 95 +++ .../Goals/Templates/GoalTemplateGetCommand.cs | 93 +++ .../Recovery/Jobs/RecoveryJobGetCommand.cs | 95 +++ .../RecoveryJobResourceGetCommand.cs | 97 +++ .../Recovery/Plans/RecoveryPlanGetCommand.cs | 93 +++ .../Resources/RecoveryResourceGetCommand.cs | 95 +++ .../ResilienceManagementJsonContext.cs | 58 ++ .../UsagePlanEnrollmentGetCommand.cs | 94 +++ .../UsagePlans/UsagePlanGetCommand.cs | 108 +++ .../src/GlobalUsings.cs | 4 + .../src/Models/GoalAssignmentInfo.cs | 22 + .../src/Models/GoalAssignmentKind.cs | 9 + .../src/Models/GoalRequirement.cs | 10 + .../src/Models/GoalResourceInfo.cs | 32 + .../src/Models/GoalTemplateInfo.cs | 25 + .../src/Models/GoalTemplateKind.cs | 9 + .../src/Models/ResourceSummary.cs | 10 + .../src/Models/UsagePlanEnrollmentInfo.cs | 26 + .../src/Models/UsagePlanInfo.cs | 22 + .../src/Models/UsagePlanKind.cs | 10 + .../src/Options/Drills/DrillGetOption.cs | 25 + .../Resources/DrillResourceGetOption.cs | 28 + .../Options/Drills/Runs/DrillRunGetOption.cs | 28 + .../Resources/DrillRunResourceGetOption.cs | 31 + .../Assignments/GoalAssignmentGetOption.cs | 25 + .../Goals/Resources/GoalResourceGetOption.cs | 28 + .../Goals/Templates/GoalTemplateGetOption.cs | 25 + .../Recovery/Jobs/RecoveryJobGetOption.cs | 28 + .../Resources/RecoveryJobResourceGetOption.cs | 31 + .../Recovery/Plans/RecoveryPlanGetOption.cs | 25 + .../Resources/RecoveryResourceGetOption.cs | 28 + .../ResilienceManagementOptionDefinitions.cs | 15 + .../UsagePlanEnrollmentGetOption.cs | 28 + .../Options/UsagePlans/UsagePlanGetOption.cs | 25 + .../src/ResilienceManagementSetup.cs | 146 ++++ .../Services/IReseilienceManagementService.cs | 66 ++ .../Services/ResilienceManagementService.cs | 635 ++++++++++++++++++ .../AssemblyAttributes.cs | 5 + ...cp.Tools.ResilienceManagement.Tests.csproj | 21 + .../Drills/DrillGetCommandTests.cs | 101 +++ .../Drills/DrillListCommandTests.cs | 100 +++ .../GoalAssignmentCreateCommandTests.cs | 112 +++ .../GoalAssignmentGetCommandTests.cs | 102 +++ .../GoalAssignmentListCommandTests.cs | 100 +++ .../Resources/GoalResourceGetCommandTests.cs | 106 +++ .../Resources/GoalResourceListCommandTests.cs | 104 +++ .../GoalTemplateCreateCommandTests.cs | 122 ++++ .../Templates/GoalTemplateGetCommandTests.cs | 102 +++ .../Templates/GoalTemplateListCommandTests.cs | 100 +++ .../Plans/RecoveryPlanGetCommandTests.cs | 101 +++ .../Plans/RecoveryPlanListCommandTests.cs | 100 +++ .../ResilienceManagementCommandTests.cs | 480 +++++++++++++ .../UsagePlanEnrollmentCreateCommandTests.cs | 113 ++++ .../UsagePlanEnrollmentGetCommandTests.cs | 106 +++ .../UsagePlanEnrollmentListCommandTests.cs | 104 +++ .../UsagePlans/UsagePlanCreateCommandTests.cs | 129 ++++ .../UsagePlans/UsagePlanGetCommandTests.cs | 102 +++ .../UsagePlans/UsagePlanListCommandTests.cs | 124 ++++ .../assets.json | 6 + .../tests/test-resources-post.ps1 | 19 + .../tests/test-resources.bicep | 53 ++ 71 files changed, 5241 insertions(+) create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/AssemblyInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Azure.Mcp.Tools.ResilienceManagement.csproj create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/DrillGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Resources/DrillResourceGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/DrillRunGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/Resources/DrillRunResourceGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Assignments/GoalAssignmentGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Resources/GoalResourceGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Templates/GoalTemplateGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/RecoveryJobGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/Resources/RecoveryJobResourceGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/RecoveryPlanGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/Resources/RecoveryResourceGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/ResilienceManagementJsonContext.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/UsagePlanGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentKind.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalRequirement.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalResourceInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateKind.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/ResourceSummary.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanEnrollmentInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanInfo.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanKind.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/DrillGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Resources/DrillResourceGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/DrillRunGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/Resources/DrillRunResourceGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Assignments/GoalAssignmentGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Resources/GoalResourceGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Templates/GoalTemplateGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/RecoveryJobGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/Resources/RecoveryJobResourceGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/RecoveryPlanGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/Resources/RecoveryResourceGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/ResilienceManagementOptionDefinitions.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/Enrollments/UsagePlanEnrollmentGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/UsagePlanGetOption.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/ResilienceManagementSetup.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep diff --git a/Directory.Packages.props b/Directory.Packages.props index 136351699a..6b08f9f8ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,6 +58,7 @@ + diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 63ed2d9daa..68a606aa88 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -372,6 +372,13 @@ + + + + + + + diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs index c9c1bfead2..7ec75aa81f 100644 --- a/servers/Azure.Mcp.Server/src/Program.cs +++ b/servers/Azure.Mcp.Server/src/Program.cs @@ -222,6 +222,7 @@ private static IAreaSetup[] RegisterAreas() new Azure.Mcp.Tools.Postgres.PostgresSetup(), new Azure.Mcp.Tools.Pricing.PricingSetup(), new Azure.Mcp.Tools.Redis.RedisSetup(), + new Azure.Mcp.Tools.ResilienceManagement.ResilienceManagementSetup(), new Azure.Mcp.Tools.ResourceHealth.ResourceHealthSetup(), new Azure.Mcp.Tools.Search.SearchSetup(), new Azure.Mcp.Tools.Speech.SpeechSetup(), diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/AssemblyInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/AssemblyInfo.cs new file mode 100644 index 0000000000..be08af097f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Azure.Mcp.Tools.ResilienceManagement.Tests")] diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Azure.Mcp.Tools.ResilienceManagement.csproj b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Azure.Mcp.Tools.ResilienceManagement.csproj new file mode 100644 index 0000000000..b9064cdc62 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Azure.Mcp.Tools.ResilienceManagement.csproj @@ -0,0 +1,19 @@ + + + true + + + + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/DrillGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/DrillGetCommand.cs new file mode 100644 index 0000000000..90bca37619 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/DrillGetCommand.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; + +[CommandMetadata( + Id = "f0b6d31a-2c84-4e79-9a51-7d3f8c0b2e65", + Name = "get", + Title = "Get or List Resilience Drills", + Description = """ + Gets resilience drills in the specified service group. Provide a drill name to get the full details of + that drill (including its identity, properties, and provisioning state). Omit the name to list all + drills in the service group, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class DrillGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, DrillGetOptions options, CancellationToken cancellationToken) + { + try + { + DrillGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var drills = await _resilienceManagementService.ListDrillsAsync( + options.ServiceGroup, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillGetCommandResult(Drills: drills.ToList()); + } + else + { + var drill = await _resilienceManagementService.GetDrillAsync( + options.ServiceGroup, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillGetCommandResult(Drill: drill); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.DrillGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting drill(s). ServiceGroup: {ServiceGroup}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Drill not found. Verify the drill name, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the drill. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Drill not found. Verify the drill and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record DrillGetCommandResult(List? Drills = null, JsonElement Drill = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Resources/DrillResourceGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Resources/DrillResourceGetCommand.cs new file mode 100644 index 0000000000..9ce7f66612 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Resources/DrillResourceGetCommand.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Resources; + +[CommandMetadata( + Id = "a4c8e1f2-7b39-4d06-8e12-5c9a0d4b6f31", + Name = "get", + Title = "Get or List Resilience Drill Resources", + Description = """ + Gets the resources (targets) of a resilience drill. Provide a drill resource name to get the full + details of that resource. Omit the name to list all resources of the drill, returning only their id + and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class DrillResourceGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, DrillResourceGetOptions options, CancellationToken cancellationToken) + { + try + { + DrillResourceGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var drillResources = await _resilienceManagementService.ListDrillResourcesAsync( + options.ServiceGroup, + options.Drill, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillResourceGetCommandResult(DrillResources: drillResources.ToList()); + } + else + { + var drillResource = await _resilienceManagementService.GetDrillResourceAsync( + options.ServiceGroup, + options.Drill, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillResourceGetCommandResult(DrillResource: drillResource); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.DrillResourceGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting drill resource(s). ServiceGroup: {ServiceGroup}, Drill: {Drill}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Drill, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Drill resource not found. Verify the drill resource name, drill, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the drill resource. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Drill resource not found. Verify the drill resource and drill exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record DrillResourceGetCommandResult(List? DrillResources = null, JsonElement DrillResource = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/DrillRunGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/DrillRunGetCommand.cs new file mode 100644 index 0000000000..c67534b858 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/DrillRunGetCommand.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Runs; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs; + +[CommandMetadata( + Id = "d5f8b1c3-6a27-4e94-8c50-2b7d9f0a3e16", + Name = "get", + Title = "Get or List Resilience Drill Runs", + Description = """ + Gets the runs of a resilience drill. Provide a drill run name to get the full details of that run. Omit + the name to list all runs of the drill, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class DrillRunGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, DrillRunGetOptions options, CancellationToken cancellationToken) + { + try + { + DrillRunGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var drillRuns = await _resilienceManagementService.ListDrillRunsAsync( + options.ServiceGroup, + options.Drill, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillRunGetCommandResult(DrillRuns: drillRuns.ToList()); + } + else + { + var drillRun = await _resilienceManagementService.GetDrillRunAsync( + options.ServiceGroup, + options.Drill, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillRunGetCommandResult(DrillRun: drillRun); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.DrillRunGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting drill run(s). ServiceGroup: {ServiceGroup}, Drill: {Drill}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Drill, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Drill run not found. Verify the drill run name, drill, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the drill run. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Drill run not found. Verify the drill run and drill exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record DrillRunGetCommandResult(List? DrillRuns = null, JsonElement DrillRun = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/Resources/DrillRunResourceGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/Resources/DrillRunResourceGetCommand.cs new file mode 100644 index 0000000000..15ac9c88e8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Drills/Runs/Resources/DrillRunResourceGetCommand.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Runs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs.Resources; + +[CommandMetadata( + Id = "f3b7d2a8-1c64-4e90-9b25-6a0f8c3d5e74", + Name = "get", + Title = "Get or List Resilience Drill Run Resources", + Description = """ + Gets the resources (targets) of a resilience drill run. Provide a drill run resource name to get the + full details of that resource. Omit the name to list all resources of the drill run, returning only + their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class DrillRunResourceGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, DrillRunResourceGetOptions options, CancellationToken cancellationToken) + { + try + { + DrillRunResourceGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var drillRunResources = await _resilienceManagementService.ListDrillRunResourcesAsync( + options.ServiceGroup, + options.Drill, + options.DrillRun, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillRunResourceGetCommandResult(DrillRunResources: drillRunResources.ToList()); + } + else + { + var drillRunResource = await _resilienceManagementService.GetDrillRunResourceAsync( + options.ServiceGroup, + options.Drill, + options.DrillRun, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new DrillRunResourceGetCommandResult(DrillRunResource: drillRunResource); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.DrillRunResourceGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting drill run resource(s). ServiceGroup: {ServiceGroup}, Drill: {Drill}, DrillRun: {DrillRun}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Drill, options.DrillRun, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Drill run resource not found. Verify the drill run resource name, drill run, drill, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the drill run resource. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Drill run resource not found. Verify the drill run resource, drill run, and drill exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record DrillRunResourceGetCommandResult(List? DrillRunResources = null, JsonElement DrillRunResource = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Assignments/GoalAssignmentGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Assignments/GoalAssignmentGetCommand.cs new file mode 100644 index 0000000000..48f3c06e37 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Assignments/GoalAssignmentGetCommand.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; + +[CommandMetadata( + Id = "c1f4a82e-6d39-4b57-9e08-2a7c5b9d4f61", + Name = "get", + Title = "Get or List Resilience Goal Assignments", + Description = """ + Gets resilience goal assignments in the specified service group. Provide a goal assignment name to get + the full details of that assignment (id, name, goal assignment type, goal template id, and provisioning + state). Omit the name to list all goal assignments in the service group, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class GoalAssignmentGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, GoalAssignmentGetOptions options, CancellationToken cancellationToken) + { + try + { + GoalAssignmentGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var goalAssignments = await _resilienceManagementService.ListGoalAssignmentsAsync( + options.ServiceGroup, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalAssignmentGetCommandResult(GoalAssignments: goalAssignments.ToList()); + } + else + { + var goalAssignment = await _resilienceManagementService.GetGoalAssignmentAsync( + options.ServiceGroup, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalAssignmentGetCommandResult(GoalAssignment: goalAssignment); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.GoalAssignmentGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting goal assignment(s). ServiceGroup: {ServiceGroup}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Goal assignment not found. Verify the goal assignment name, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the goal assignment. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Goal assignment not found. Verify the goal assignment and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record GoalAssignmentGetCommandResult(List? GoalAssignments = null, GoalAssignmentInfo? GoalAssignment = null); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Resources/GoalResourceGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Resources/GoalResourceGetCommand.cs new file mode 100644 index 0000000000..57f4fcf336 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Resources/GoalResourceGetCommand.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; + +[CommandMetadata( + Id = "a3e7f29c-5b81-4d06-8f53-2c9b1e7a4d68", + Name = "get", + Title = "Get or List Resilience Goal Resources", + Description = """ + Gets the resources (members) of a resilience goal assignment. Provide a goal resource name to get the + full details of that resource (id, name, disaster recovery and high availability attestation status and + goal participation, exclusion reasons, provisioning state, the resource ARM id, and service group + memberships). Omit the name to list all resources of the goal assignment, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class GoalResourceGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, GoalResourceGetOptions options, CancellationToken cancellationToken) + { + try + { + GoalResourceGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var goalResources = await _resilienceManagementService.ListGoalResourcesAsync( + options.ServiceGroup, + options.GoalAssignment, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalResourceGetCommandResult(GoalResources: goalResources.ToList()); + } + else + { + var goalResource = await _resilienceManagementService.GetGoalResourceAsync( + options.ServiceGroup, + options.GoalAssignment, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalResourceGetCommandResult(GoalResource: goalResource); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.GoalResourceGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting goal resource(s). ServiceGroup: {ServiceGroup}, GoalAssignment: {GoalAssignment}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.GoalAssignment, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Goal resource not found. Verify the goal resource name, goal assignment, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the goal resource. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Goal resource not found. Verify the goal resource, goal assignment, and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record GoalResourceGetCommandResult(List? GoalResources = null, GoalResourceInfo? GoalResource = null); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Templates/GoalTemplateGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Templates/GoalTemplateGetCommand.cs new file mode 100644 index 0000000000..1af722e39c --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Goals/Templates/GoalTemplateGetCommand.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; + +[CommandMetadata( + Id = "e5b71c93-4d8a-42f6-9b0c-3a7e1d6f8025", + Name = "get", + Title = "Get or List Resilience Goal Templates", + Description = """ + Gets resilience goal templates in the specified service group. Provide a goal template name to get the + full details of that template (id, name, goal type, provisioning state, recovery point and time + objectives, and high availability and disaster recovery requirements). Omit the name to list all goal + templates in the service group, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class GoalTemplateGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, GoalTemplateGetOptions options, CancellationToken cancellationToken) + { + try + { + GoalTemplateGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var goalTemplates = await _resilienceManagementService.ListGoalTemplatesAsync( + options.ServiceGroup, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalTemplateGetCommandResult(GoalTemplates: goalTemplates.ToList()); + } + else + { + var goalTemplate = await _resilienceManagementService.GetGoalTemplateAsync( + options.ServiceGroup, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new GoalTemplateGetCommandResult(GoalTemplate: goalTemplate); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.GoalTemplateGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting goal template(s). ServiceGroup: {ServiceGroup}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Goal template not found. Verify the goal template name, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the goal template. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Goal template not found. Verify the goal template and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record GoalTemplateGetCommandResult(List? GoalTemplates = null, GoalTemplateInfo? GoalTemplate = null); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/RecoveryJobGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/RecoveryJobGetCommand.cs new file mode 100644 index 0000000000..05cd04404d --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/RecoveryJobGetCommand.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Jobs; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs; + +[CommandMetadata( + Id = "c7e1b4a9-2d63-4f08-8b95-1a6c3f0d7e42", + Name = "get", + Title = "Get or List Resilience Recovery Jobs", + Description = """ + Gets the recovery jobs of a resilience recovery plan. Provide a recovery job name to get the full + details of that job. Omit the name to list all recovery jobs of the recovery plan, returning only their + id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class RecoveryJobGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, RecoveryJobGetOptions options, CancellationToken cancellationToken) + { + try + { + RecoveryJobGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var recoveryJobs = await _resilienceManagementService.ListRecoveryJobsAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryJobGetCommandResult(RecoveryJobs: recoveryJobs.ToList()); + } + else + { + var recoveryJob = await _resilienceManagementService.GetRecoveryJobAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryJobGetCommandResult(RecoveryJob: recoveryJob); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.RecoveryJobGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting recovery job(s). ServiceGroup: {ServiceGroup}, RecoveryPlan: {RecoveryPlan}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.RecoveryPlan, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Recovery job not found. Verify the recovery job name, recovery plan, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the recovery job. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Recovery job not found. Verify the recovery job, recovery plan, and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record RecoveryJobGetCommandResult(List? RecoveryJobs = null, JsonElement RecoveryJob = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/Resources/RecoveryJobResourceGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/Resources/RecoveryJobResourceGetCommand.cs new file mode 100644 index 0000000000..f007e3ad72 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Jobs/Resources/RecoveryJobResourceGetCommand.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Jobs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs.Resources; + +[CommandMetadata( + Id = "a1d6f3c8-7b94-4e25-9c08-2f5b7d0a3e69", + Name = "get", + Title = "Get or List Resilience Recovery Job Resources", + Description = """ + Gets the resources (targets) of a resilience recovery job. Provide a recovery job resource name to get + the full details of that resource. Omit the name to list all resources of the recovery job, returning + only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class RecoveryJobResourceGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, RecoveryJobResourceGetOptions options, CancellationToken cancellationToken) + { + try + { + RecoveryJobResourceGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var recoveryJobResources = await _resilienceManagementService.ListRecoveryJobResourcesAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.RecoveryJob, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryJobResourceGetCommandResult(RecoveryJobResources: recoveryJobResources.ToList()); + } + else + { + var recoveryJobResource = await _resilienceManagementService.GetRecoveryJobResourceAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.RecoveryJob, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryJobResourceGetCommandResult(RecoveryJobResource: recoveryJobResource); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.RecoveryJobResourceGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting recovery job resource(s). ServiceGroup: {ServiceGroup}, RecoveryPlan: {RecoveryPlan}, RecoveryJob: {RecoveryJob}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.RecoveryPlan, options.RecoveryJob, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Recovery job resource not found. Verify the recovery job resource name, recovery job, recovery plan, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the recovery job resource. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Recovery job resource not found. Verify the recovery job resource, recovery job, recovery plan, and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record RecoveryJobResourceGetCommandResult(List? RecoveryJobResources = null, JsonElement RecoveryJobResource = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/RecoveryPlanGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/RecoveryPlanGetCommand.cs new file mode 100644 index 0000000000..9eb655a3da --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/RecoveryPlanGetCommand.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; + +[CommandMetadata( + Id = "d4a8f1c6-3b75-4e29-9c08-2f6b5d0a7e31", + Name = "get", + Title = "Get or List Resilience Recovery Plans", + Description = """ + Gets resilience recovery plans in the specified service group. Provide a recovery plan name to get the + full details of that plan (including its properties and provisioning state). Omit the name to list all + recovery plans in the service group, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class RecoveryPlanGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, RecoveryPlanGetOptions options, CancellationToken cancellationToken) + { + try + { + RecoveryPlanGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var recoveryPlans = await _resilienceManagementService.ListRecoveryPlansAsync( + options.ServiceGroup, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryPlanGetCommandResult(RecoveryPlans: recoveryPlans.ToList()); + } + else + { + var recoveryPlan = await _resilienceManagementService.GetRecoveryPlanAsync( + options.ServiceGroup, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryPlanGetCommandResult(RecoveryPlan: recoveryPlan); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.RecoveryPlanGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting recovery plan(s). ServiceGroup: {ServiceGroup}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Recovery plan not found. Verify the recovery plan name, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the recovery plan. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Recovery plan not found. Verify the recovery plan and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record RecoveryPlanGetCommandResult(List? RecoveryPlans = null, JsonElement RecoveryPlan = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/Resources/RecoveryResourceGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/Resources/RecoveryResourceGetCommand.cs new file mode 100644 index 0000000000..27c2a01881 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/Recovery/Plans/Resources/RecoveryResourceGetCommand.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Plans.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans.Resources; + +[CommandMetadata( + Id = "b9d2f74c-1a85-4e30-8c67-3f0b6a2d9e51", + Name = "get", + Title = "Get or List Resilience Recovery Resources", + Description = """ + Gets the resources (members) of a resilience recovery plan. Provide a recovery resource name to get the + full details of that resource. Omit the name to list all resources of the recovery plan, returning only + their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class RecoveryResourceGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, RecoveryResourceGetOptions options, CancellationToken cancellationToken) + { + try + { + RecoveryResourceGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var recoveryResources = await _resilienceManagementService.ListRecoveryResourcesAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryResourceGetCommandResult(RecoveryResources: recoveryResources.ToList()); + } + else + { + var recoveryResource = await _resilienceManagementService.GetRecoveryResourceAsync( + options.ServiceGroup, + options.RecoveryPlan, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new RecoveryResourceGetCommandResult(RecoveryResource: recoveryResource); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.RecoveryResourceGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting recovery resource(s). ServiceGroup: {ServiceGroup}, RecoveryPlan: {RecoveryPlan}, Name: {Name}, Subscription: {Subscription}.", + options.ServiceGroup, options.RecoveryPlan, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Recovery resource not found. Verify the recovery resource name, recovery plan, service group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the recovery resource. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Recovery resource not found. Verify the recovery resource, recovery plan, and service group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record RecoveryResourceGetCommandResult(List? RecoveryResources = null, JsonElement RecoveryResource = default); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/ResilienceManagementJsonContext.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/ResilienceManagementJsonContext.cs new file mode 100644 index 0000000000..ac2e146cc5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/ResilienceManagementJsonContext.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Models; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands; + +[JsonSerializable(typeof(GoalTemplateGetCommand.GoalTemplateGetCommandResult))] +[JsonSerializable(typeof(GoalTemplateInfo))] +[JsonSerializable(typeof(GoalTemplateInfoProperties))] +[JsonSerializable(typeof(GoalTemplateInfoSystemData))] +[JsonSerializable(typeof(GoalAssignmentGetCommand.GoalAssignmentGetCommandResult))] +[JsonSerializable(typeof(GoalAssignmentInfo))] +[JsonSerializable(typeof(GoalAssignmentInfoProperties))] +[JsonSerializable(typeof(GoalAssignmentInfoSystemData))] +[JsonSerializable(typeof(GoalResourceGetCommand.GoalResourceGetCommandResult))] +[JsonSerializable(typeof(GoalResourceInfo))] +[JsonSerializable(typeof(GoalResourceInfoProperties))] +[JsonSerializable(typeof(GoalResourceServiceGroupMembership))] +[JsonSerializable(typeof(GoalResourceInfoSystemData))] +[JsonSerializable(typeof(UsagePlanGetCommand.UsagePlanGetCommandResult))] +[JsonSerializable(typeof(UsagePlanInfo))] +[JsonSerializable(typeof(UsagePlanInfoProperties))] +[JsonSerializable(typeof(UsagePlanInfoSystemData))] +[JsonSerializable(typeof(UsagePlanEnrollmentGetCommand.UsagePlanEnrollmentGetCommandResult))] +[JsonSerializable(typeof(UsagePlanEnrollmentInfo))] +[JsonSerializable(typeof(UsagePlanEnrollmentInfoProperties))] +[JsonSerializable(typeof(UsagePlanEnrollmentInfoErrorDetails))] +[JsonSerializable(typeof(UsagePlanEnrollmentInfoSystemData))] +[JsonSerializable(typeof(DrillGetCommand.DrillGetCommandResult))] +[JsonSerializable(typeof(DrillResourceGetCommand.DrillResourceGetCommandResult))] +[JsonSerializable(typeof(DrillRunGetCommand.DrillRunGetCommandResult))] +[JsonSerializable(typeof(DrillRunResourceGetCommand.DrillRunResourceGetCommandResult))] +[JsonSerializable(typeof(RecoveryPlanGetCommand.RecoveryPlanGetCommandResult))] +[JsonSerializable(typeof(RecoveryResourceGetCommand.RecoveryResourceGetCommandResult))] +[JsonSerializable(typeof(RecoveryJobGetCommand.RecoveryJobGetCommandResult))] +[JsonSerializable(typeof(RecoveryJobResourceGetCommand.RecoveryJobResourceGetCommandResult))] +[JsonSerializable(typeof(ResourceSummary))] +[JsonSerializable(typeof(JsonElement))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)] +internal sealed partial class ResilienceManagementJsonContext : JsonSerializerContext +{ +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommand.cs new file mode 100644 index 0000000000..776186cd41 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommand.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; + +[CommandMetadata( + Id = "f6c29a47-8b51-43d0-9e62-1d4a7c8e9b50", + Name = "get", + Title = "Get or List Resilience Usage Plan Enrollments", + Description = """ + Gets enrollments of a resilience usage plan. Provide an enrollment name to get the full details of that + enrollment (id, name, the associated service group id, provisioning state, and error details). Omit the + name to list all enrollments of the usage plan, returning only their id and name. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class UsagePlanEnrollmentGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, UsagePlanEnrollmentGetOptions options, CancellationToken cancellationToken) + { + try + { + UsagePlanEnrollmentGetCommandResult result; + if (string.IsNullOrEmpty(options.Name)) + { + var enrollments = await _resilienceManagementService.ListUsagePlanEnrollmentsAsync( + options.ResourceGroup, + options.UsagePlan, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new UsagePlanEnrollmentGetCommandResult(Enrollments: enrollments.ToList()); + } + else + { + var enrollment = await _resilienceManagementService.GetUsagePlanEnrollmentAsync( + options.ResourceGroup, + options.UsagePlan, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new UsagePlanEnrollmentGetCommandResult(Enrollment: enrollment); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.UsagePlanEnrollmentGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting usage plan enrollment(s). ResourceGroup: {ResourceGroup}, UsagePlan: {UsagePlan}, Name: {Name}, Subscription: {Subscription}.", + options.ResourceGroup, options.UsagePlan, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Usage plan enrollment not found. Verify the enrollment name, usage plan, resource group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the usage plan enrollment. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Usage plan enrollment not found. Verify the enrollment and usage plan exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record UsagePlanEnrollmentGetCommandResult(List? Enrollments = null, UsagePlanEnrollmentInfo? Enrollment = null); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/UsagePlanGetCommand.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/UsagePlanGetCommand.cs new file mode 100644 index 0000000000..85fc6207c9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Commands/UsagePlans/UsagePlanGetCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Options.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; + +[CommandMetadata( + Id = "a8d3f60b-5c27-4e19-8f74-0b6a2d9c5e83", + Name = "get", + Title = "Get or List Resilience Usage Plans", + Description = """ + Gets resilience usage plans. Provide a usage plan name (with its resource group) to get the full details + of that plan (id, name, resource type, location, tags, plan type, and provisioning state). Omit the name + to list usage plans (id and name only): for the given resource group, or for the whole subscription when + no resource group is provided. + """, + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false)] +public sealed class UsagePlanGetCommand(ILogger logger, IResilienceManagementService resilienceManagementService, ISubscriptionResolver subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) +{ + private readonly ILogger _logger = logger; + private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService; + + public override async Task ExecuteAsync(CommandContext context, UsagePlanGetOptions options, CancellationToken cancellationToken) + { + try + { + UsagePlanGetCommandResult result; + if (!string.IsNullOrEmpty(options.Name)) + { + if (string.IsNullOrEmpty(options.ResourceGroup)) + { + throw new ArgumentException("A resource group is required when getting a specific usage plan. Provide --resource-group."); + } + + var usagePlan = await _resilienceManagementService.GetUsagePlanAsync( + options.ResourceGroup, + options.Name, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new UsagePlanGetCommandResult(UsagePlan: usagePlan); + } + else if (!string.IsNullOrEmpty(options.ResourceGroup)) + { + var usagePlans = await _resilienceManagementService.ListUsagePlansAsync( + options.ResourceGroup, + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new UsagePlanGetCommandResult(UsagePlans: usagePlans.ToList()); + } + else + { + var usagePlans = await _resilienceManagementService.ListUsagePlansBySubscriptionAsync( + options.Subscription!, + options.Tenant, + options.RetryPolicy, + cancellationToken); + result = new UsagePlanGetCommandResult(UsagePlans: usagePlans.ToList()); + } + + context.Response.Results = ResponseResult.Create( + result, + ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error getting usage plan(s). ResourceGroup: {ResourceGroup}, Name: {Name}, Subscription: {Subscription}.", + options.ResourceGroup, options.Name, options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + KeyNotFoundException => "Usage plan not found. Verify the usage plan name, resource group, subscription, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed getting the usage plan. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Usage plan not found. Verify the usage plan and resource group exist and you have access.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + public record UsagePlanGetCommandResult(List? UsagePlans = null, UsagePlanInfo? UsagePlan = null); +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs new file mode 100644 index 0000000000..9e46d092bc --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.CommandLine; \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentInfo.cs new file mode 100644 index 0000000000..d7c169a93d --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentInfo.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record GoalAssignmentInfo( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("properties")] GoalAssignmentInfoProperties? Properties = null, + [property: JsonPropertyName("systemData")] GoalAssignmentInfoSystemData? SystemData = null); + +public sealed record GoalAssignmentInfoProperties( + [property: JsonPropertyName("goalAssignmentType")] string GoalAssignmentType, + [property: JsonPropertyName("goalTemplateId")] string GoalTemplateId, + [property: JsonPropertyName("provisioningState")] string ProvisioningState); + +public sealed record GoalAssignmentInfoSystemData( + [property: JsonPropertyName("createdAt")] string CreatedAt, + [property: JsonPropertyName("createdBy")] string CreatedBy, + [property: JsonPropertyName("createdByType")] string CreatedByType, + [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, + [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy, + [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentKind.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentKind.cs new file mode 100644 index 0000000000..c62fcba981 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalAssignmentKind.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public enum GoalAssignmentKind +{ + Resiliency +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalRequirement.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalRequirement.cs new file mode 100644 index 0000000000..7f12a731d6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalRequirement.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public enum GoalRequirement +{ + Required, + NotRequired +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalResourceInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalResourceInfo.cs new file mode 100644 index 0000000000..31dd258d68 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalResourceInfo.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record GoalResourceInfo( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("properties")] GoalResourceInfoProperties? Properties = null, + [property: JsonPropertyName("systemData")] GoalResourceInfoSystemData? SystemData = null); + +public sealed record GoalResourceInfoProperties( + [property: JsonPropertyName("disasterRecoveryAttestationStatus")] string DisasterRecoveryAttestationStatus, + [property: JsonPropertyName("disasterRecoveryGoalParticipation")] string DisasterRecoveryGoalParticipation, + [property: JsonPropertyName("exclusionReasonForDisasterRecoveryGoals")] string ExclusionReasonForDisasterRecoveryGoals, + [property: JsonPropertyName("exclusionReasonForHighAvailabilityGoals")] string ExclusionReasonForHighAvailabilityGoals, + [property: JsonPropertyName("highAvailabilityAttestationStatus")] string HighAvailabilityAttestationStatus, + [property: JsonPropertyName("highAvailabilityGoalParticipation")] string HighAvailabilityGoalParticipation, + [property: JsonPropertyName("provisioningState")] string ProvisioningState, + [property: JsonPropertyName("resourceArmId")] string ResourceArmId, + [property: JsonPropertyName("serviceGroupMemberships")] IReadOnlyList? ServiceGroupMemberships = null); + +public sealed record GoalResourceServiceGroupMembership( + [property: JsonPropertyName("membershipType")] string MembershipType, + [property: JsonPropertyName("serviceGroupId")] string ServiceGroupId); + +public sealed record GoalResourceInfoSystemData( + [property: JsonPropertyName("createdAt")] string CreatedAt, + [property: JsonPropertyName("createdBy")] string CreatedBy, + [property: JsonPropertyName("createdByType")] string CreatedByType, + [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, + [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy, + [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs new file mode 100644 index 0000000000..afbdd0ea08 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record GoalTemplateInfo( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("properties")] GoalTemplateInfoProperties? Properties = null, + [property: JsonPropertyName("systemData")] GoalTemplateInfoSystemData? SystemData = null); + +public sealed record GoalTemplateInfoProperties( + [property: JsonPropertyName("goalType")] string GoalType, + [property: JsonPropertyName("provisioningState")] string ProvisioningState, + [property: JsonPropertyName("regionalRecoveryPointObjective")] string RegionalRecoveryPointObjective, + [property: JsonPropertyName("regionalRecoveryTimeObjective")] string RegionalRecoveryTimeObjective, + [property: JsonPropertyName("requireDisasterRecovery")] string RequireDisasterRecovery, + [property: JsonPropertyName("requireHighAvailability")] string RequireHighAvailability); + +public sealed record GoalTemplateInfoSystemData( + [property: JsonPropertyName("createdAt")] string CreatedAt, + [property: JsonPropertyName("createdBy")] string CreatedBy, + [property: JsonPropertyName("createdByType")] string CreatedByType, + [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, + [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy, + [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateKind.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateKind.cs new file mode 100644 index 0000000000..25d6d10e9b --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateKind.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public enum GoalTemplateKind +{ + Resiliency +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/ResourceSummary.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/ResourceSummary.cs new file mode 100644 index 0000000000..e49c999a8c --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/ResourceSummary.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record ResourceSummary( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanEnrollmentInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanEnrollmentInfo.cs new file mode 100644 index 0000000000..c7ad8042c2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanEnrollmentInfo.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record UsagePlanEnrollmentInfo( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("properties")] UsagePlanEnrollmentInfoProperties? Properties = null, + [property: JsonPropertyName("systemData")] UsagePlanEnrollmentInfoSystemData? SystemData = null); + +public sealed record UsagePlanEnrollmentInfoProperties( + [property: JsonPropertyName("serviceGroupId")] string ServiceGroupId, + [property: JsonPropertyName("provisioningState")] string ProvisioningState, + [property: JsonPropertyName("errorDetails")] UsagePlanEnrollmentInfoErrorDetails? ErrorDetails = null); + +public sealed record UsagePlanEnrollmentInfoErrorDetails( + [property: JsonPropertyName("code")] string Code, + [property: JsonPropertyName("message")] string Message); + +public sealed record UsagePlanEnrollmentInfoSystemData( + [property: JsonPropertyName("createdAt")] string CreatedAt, + [property: JsonPropertyName("createdBy")] string CreatedBy, + [property: JsonPropertyName("createdByType")] string CreatedByType, + [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, + [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy, + [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanInfo.cs new file mode 100644 index 0000000000..58f6931da8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanInfo.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public sealed record UsagePlanInfo( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("resourceType")] string ResourceType, + [property: JsonPropertyName("location")] string Location, + [property: JsonPropertyName("tags")] IReadOnlyDictionary? Tags = null, + [property: JsonPropertyName("properties")] UsagePlanInfoProperties? Properties = null, + [property: JsonPropertyName("systemData")] UsagePlanInfoSystemData? SystemData = null); + +public sealed record UsagePlanInfoProperties( + [property: JsonPropertyName("planType")] string PlanType, + [property: JsonPropertyName("provisioningState")] string ProvisioningState); + +public sealed record UsagePlanInfoSystemData( + [property: JsonPropertyName("createdAt")] string CreatedAt, + [property: JsonPropertyName("createdBy")] string CreatedBy, + [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, + [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanKind.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanKind.cs new file mode 100644 index 0000000000..3e3edefab2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/UsagePlanKind.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.ResilienceManagement.Models; + +public enum UsagePlanKind +{ + Basic, + Standard +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/DrillGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/DrillGetOption.cs new file mode 100644 index 0000000000..6a0cf4e8dc --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/DrillGetOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills; + +public class DrillGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the drill. Provide this argument to get the details of a particular drill; omit it to list all drills in the service group (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Resources/DrillResourceGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Resources/DrillResourceGetOption.cs new file mode 100644 index 0000000000..b6f7ac0e2f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Resources/DrillResourceGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Resources; + +public class DrillResourceGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the drill.")] + public required string Drill { get; set; } + + [Option(Description = "The name of the drill resource (target). Provide this argument to get the details of a particular drill resource; omit it to list all resources (targets) of the drill (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/DrillRunGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/DrillRunGetOption.cs new file mode 100644 index 0000000000..14928bce93 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/DrillRunGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Runs; + +public class DrillRunGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the drill.")] + public required string Drill { get; set; } + + [Option(Description = "The name of the drill run. Provide this argument to get the details of a particular drill run; omit it to list all runs of the drill (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/Resources/DrillRunResourceGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/Resources/DrillRunResourceGetOption.cs new file mode 100644 index 0000000000..7e8e12b2f9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Drills/Runs/Resources/DrillRunResourceGetOption.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills.Runs.Resources; + +public class DrillRunResourceGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the drill.")] + public required string Drill { get; set; } + + [Option(Description = "The name of the drill run.")] + public required string DrillRun { get; set; } + + [Option(Description = "The name of the drill run resource (target). Provide this argument to get the details of a particular drill run resource; omit it to list all resources (targets) of the drill run (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Assignments/GoalAssignmentGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Assignments/GoalAssignmentGetOption.cs new file mode 100644 index 0000000000..5a37fb0df3 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Assignments/GoalAssignmentGetOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Assignments; + +public class GoalAssignmentGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the goal assignment. Provide this argument to get the details of a particular goal assignment; omit it to list all goal assignments in the service group (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Resources/GoalResourceGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Resources/GoalResourceGetOption.cs new file mode 100644 index 0000000000..6a727113b9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Resources/GoalResourceGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Resources; + +public class GoalResourceGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the goal assignment.")] + public required string GoalAssignment { get; set; } + + [Option(Description = "The name of the goal resource. Provide this argument to get the details of a particular goal resource; omit it to list all resources (members) of the goal assignment (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Templates/GoalTemplateGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Templates/GoalTemplateGetOption.cs new file mode 100644 index 0000000000..f41961605f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Goals/Templates/GoalTemplateGetOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Goals.Templates; + +public class GoalTemplateGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the goal template. Provide this argument to get the details of a particular goal template; omit it to list all goal templates in the service group (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/RecoveryJobGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/RecoveryJobGetOption.cs new file mode 100644 index 0000000000..2061e14f69 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/RecoveryJobGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Jobs; + +public class RecoveryJobGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the recovery plan.")] + public required string RecoveryPlan { get; set; } + + [Option(Description = "The name of the recovery job. Provide this argument to get the details of a particular recovery job; omit it to list all recovery jobs of the recovery plan (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/Resources/RecoveryJobResourceGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/Resources/RecoveryJobResourceGetOption.cs new file mode 100644 index 0000000000..e92757a1b4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Jobs/Resources/RecoveryJobResourceGetOption.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Jobs.Resources; + +public class RecoveryJobResourceGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the recovery plan.")] + public required string RecoveryPlan { get; set; } + + [Option(Description = "The name of the recovery job.")] + public required string RecoveryJob { get; set; } + + [Option(Description = "The name of the recovery job resource (target). Provide this argument to get the details of a particular recovery job resource; omit it to list all resources (targets) of the recovery job (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/RecoveryPlanGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/RecoveryPlanGetOption.cs new file mode 100644 index 0000000000..c6929e9a60 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/RecoveryPlanGetOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Plans; + +public class RecoveryPlanGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the recovery plan. Provide this argument to get the details of a particular recovery plan; omit it to list all recovery plans in the service group (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/Resources/RecoveryResourceGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/Resources/RecoveryResourceGetOption.cs new file mode 100644 index 0000000000..fceaae6199 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/Recovery/Plans/Resources/RecoveryResourceGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.Recovery.Plans.Resources; + +public class RecoveryResourceGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the service group.")] + public required string ServiceGroup { get; set; } + + [Option(Description = "The name of the recovery plan.")] + public required string RecoveryPlan { get; set; } + + [Option(Description = "The name of the recovery resource (member). Provide this argument to get the details of a particular recovery resource; omit it to list all resources (members) of the recovery plan (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/ResilienceManagementOptionDefinitions.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/ResilienceManagementOptionDefinitions.cs new file mode 100644 index 0000000000..a645f00cb6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/ResilienceManagementOptionDefinitions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.ResilienceManagement.Options; + +public static class ResilienceManagementOptionDefinitions +{ + public const string ServiceGroupName = "service-group"; + + public static readonly Option ServiceGroup = new($"--{ServiceGroupName}") + { + Description = "The name of the service group.", + Required = true + }; +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/Enrollments/UsagePlanEnrollmentGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/Enrollments/UsagePlanEnrollmentGetOption.cs new file mode 100644 index 0000000000..a5a2d3d39f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/Enrollments/UsagePlanEnrollmentGetOption.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.UsagePlans.Enrollments; + +public class UsagePlanEnrollmentGetOptions : ISubscriptionOption +{ + [Option(Description = OptionDescriptions.ResourceGroup)] + public required string ResourceGroup { get; set; } + + [Option(Description = "The name of the usage plan.")] + public required string UsagePlan { get; set; } + + [Option(Description = "The name of the usage plan enrollment. Provide this argument to get the details of a particular enrollment; omit it to list all enrollments of the usage plan (id and name only).")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/UsagePlanGetOption.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/UsagePlanGetOption.cs new file mode 100644 index 0000000000..2854a3735a --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Options/UsagePlans/UsagePlanGetOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Options.UsagePlans; + +public class UsagePlanGetOptions : ISubscriptionOption +{ + [Option(Description = "The name of the resource group. If omitted (and no usage plan name is given), all usage plans in the subscription are listed (id and name only).")] + public string? ResourceGroup { get; set; } + + [Option(Description = "The name of the usage plan. Provide this argument to get the details of a particular usage plan (requires a resource group); omit it to list usage plans (id and name only) for the resource group, or for the whole subscription when no resource group is given.")] + public string? Name { get; set; } + + [Option(Description = OptionDescriptions.Subscription)] + public string? Subscription { get; set; } + + [Option(Description = OptionDescriptions.Tenant)] + public string? Tenant { get; set; } + + [OptionContainer(Prefix = "retry")] + public RetryPolicyOptions? RetryPolicy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/ResilienceManagementSetup.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/ResilienceManagementSetup.cs new file mode 100644 index 0000000000..0364ca8cbf --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/ResilienceManagementSetup.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Mcp.Core.Areas; +using Microsoft.Mcp.Core.Commands; + +namespace Azure.Mcp.Tools.ResilienceManagement; + +public class ResilienceManagementSetup : IAreaSetup +{ + public string Name => "resilience"; + + public string Title => "Azure Resilience Management"; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public CommandGroup RegisterCommands(IServiceProvider serviceProvider) + { + var resilienceManagement = new CommandGroup(Name, + """ + Azure Resilience Management operations - Commands for working with resilience goals and goal + templates for Azure service groups. Use this tool to list the resilience goal templates available + for a service group, including goal type, provisioning state, recovery point and time objectives, + and high availability and disaster recovery requirements. + """, + Title); + + // Create goal subgroup + var goals = new CommandGroup("goal", "Resilience goal operations - Commands for working with resilience goals and goal templates for Azure service groups."); + resilienceManagement.AddSubGroup(goals); + + // Create template subgroup under goal + var templates = new CommandGroup("template", "Resilience goal template operations - Commands for listing resilience goal templates for Azure service groups."); + goals.AddSubGroup(templates); + + // Create assignment subgroup under goal + var assignments = new CommandGroup("assignment", "Resilience goal assignment operations - Commands for listing resilience goal assignments for Azure service groups."); + goals.AddSubGroup(assignments); + + // Create resource subgroup under goal + var goalResources = new CommandGroup("resource", "Resilience goal resource operations - Commands for listing the resources (members) of a resilience goal assignment."); + goals.AddSubGroup(goalResources); + + // Register commands + templates.AddCommand(serviceProvider); + assignments.AddCommand(serviceProvider); + goalResources.AddCommand(serviceProvider); + + // Create usageplan subgroup + var usagePlans = new CommandGroup("usageplan", "Resilience usage plan operations - Commands for listing resilience usage plans in an Azure resource group."); + resilienceManagement.AddSubGroup(usagePlans); + + usagePlans.AddCommand(serviceProvider); + + // Create enrollment subgroup under usageplan + var enrollments = new CommandGroup("enrollment", "Resilience usage plan enrollment operations - Commands for listing enrollments of a resilience usage plan."); + usagePlans.AddSubGroup(enrollments); + + enrollments.AddCommand(serviceProvider); + + // Create drill subgroup + var drills = new CommandGroup("drill", "Resilience drill operations - Commands for listing and getting resilience drills for an Azure service group."); + resilienceManagement.AddSubGroup(drills); + + drills.AddCommand(serviceProvider); + + // Create resource subgroup under drill + var drillResources = new CommandGroup("resource", "Resilience drill resource operations - Commands for listing and getting the resources (targets) of a resilience drill."); + drills.AddSubGroup(drillResources); + + drillResources.AddCommand(serviceProvider); + + // Create run subgroup under drill + var drillRuns = new CommandGroup("run", "Resilience drill run operations - Commands for listing and getting the runs of a resilience drill."); + drills.AddSubGroup(drillRuns); + + drillRuns.AddCommand(serviceProvider); + + // Create resource subgroup under drill run + var drillRunResources = new CommandGroup("resource", "Resilience drill run resource operations - Commands for listing and getting the resources (targets) of a resilience drill run."); + drillRuns.AddSubGroup(drillRunResources); + + drillRunResources.AddCommand(serviceProvider); + + // Create recovery subgroup with a plan subgroup + var recovery = new CommandGroup("recovery", "Resilience recovery operations - Commands for working with resilience recovery plans for an Azure service group."); + resilienceManagement.AddSubGroup(recovery); + + var recoveryPlans = new CommandGroup("plan", "Resilience recovery plan operations - Commands for listing and getting resilience recovery plans for an Azure service group."); + recovery.AddSubGroup(recoveryPlans); + + recoveryPlans.AddCommand(serviceProvider); + + // Create resource subgroup under recovery plan + var recoveryResources = new CommandGroup("resource", "Resilience recovery resource operations - Commands for listing and getting the resources (members) of a resilience recovery plan."); + recoveryPlans.AddSubGroup(recoveryResources); + + recoveryResources.AddCommand(serviceProvider); + + // Create job subgroup under recovery + var recoveryJobs = new CommandGroup("job", "Resilience recovery job operations - Commands for listing and getting the recovery jobs of a resilience recovery plan."); + recovery.AddSubGroup(recoveryJobs); + + recoveryJobs.AddCommand(serviceProvider); + + // Create resource subgroup under recovery job + var recoveryJobResources = new CommandGroup("resource", "Resilience recovery job resource operations - Commands for listing and getting the resources (targets) of a resilience recovery job."); + recoveryJobs.AddSubGroup(recoveryJobResources); + + recoveryJobResources.AddCommand(serviceProvider); + + return resilienceManagement; + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs new file mode 100644 index 0000000000..3546cb19a7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Microsoft.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.ResilienceManagement.Services; + +public interface IResilienceManagementService +{ + Task> ListGoalTemplatesAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetGoalTemplateAsync(string serviceGroup, string goalTemplate, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListGoalAssignmentsAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetGoalAssignmentAsync(string serviceGroup, string goalAssignment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListUsagePlansAsync(string resourceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetUsagePlanAsync(string resourceGroup, string usagePlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListUsagePlansBySubscriptionAsync(string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListUsagePlanEnrollmentsAsync(string resourceGroup, string usagePlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetUsagePlanEnrollmentAsync(string resourceGroup, string usagePlan, string enrollment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListGoalResourcesAsync(string serviceGroup, string goalAssignment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetGoalResourceAsync(string serviceGroup, string goalAssignment, string goalResource, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListDrillsAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetDrillAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListDrillRunsAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetDrillRunAsync(string serviceGroup, string drill, string drillRun, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListDrillRunResourcesAsync(string serviceGroup, string drill, string drillRun, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetDrillRunResourceAsync(string serviceGroup, string drill, string drillRun, string drillRunTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListDrillResourcesAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetDrillResourceAsync(string serviceGroup, string drill, string drillTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListRecoveryPlansAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetRecoveryPlanAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListRecoveryResourcesAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetRecoveryResourceAsync(string serviceGroup, string recoveryPlan, string recoveryResource, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListRecoveryJobsAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetRecoveryJobAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task> ListRecoveryJobResourcesAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task GetRecoveryJobResourceAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string recoveryJobTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs new file mode 100644 index 0000000000..b30b203602 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs @@ -0,0 +1,635 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Core.Services.Azure.Tenant; +// using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.ResourceManager; +using Azure.ResourceManager.ResilienceManagement; +using Azure.ResourceManager.ResilienceManagement.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Options; +using Microsoft.Mcp.Core.Services.Azure.Authentication; + +namespace Azure.Mcp.Tools.ResilienceManagement.Services; + +public sealed class ResilienceManagementService( + ISubscriptionService subscriptionService, + ITenantService tenantService, + ILogger logger) + : BaseAzureResourceService(subscriptionService, tenantService), IResilienceManagementService +{ + + private readonly ISubscriptionService _subscriptionService = subscriptionService; + private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + public async Task> ListGoalTemplatesAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + GoalTemplateCollection goalTemplates = armClient.GetGoalTemplates(serviceGroupId); + + var result = new List(); + await foreach (var goalTemplate in goalTemplates.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: goalTemplate.Data.Id?.ToString() ?? string.Empty, + Name: goalTemplate.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetGoalTemplateAsync(string serviceGroup, string goalTemplate, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + GoalTemplateCollection goalTemplates = armClient.GetGoalTemplates(serviceGroupId); + GoalTemplateResource resource = await goalTemplates.GetAsync(goalTemplate, cancellationToken); + + return MapGoalTemplate(resource.Data); + } + + private static GoalTemplateInfo MapGoalTemplate(GoalTemplateData data) + { + var props = data.Properties; + var systemData = data.SystemData; + + var mappedProperties = props is null + ? null + : new GoalTemplateInfoProperties( + GoalType: props.GoalType.ToString(), + ProvisioningState: props.ProvisioningState?.ToString() ?? string.Empty, + RegionalRecoveryPointObjective: props.RegionalRecoveryPointObjective ?? string.Empty, + RegionalRecoveryTimeObjective: props.RegionalRecoveryTimeObjective ?? string.Empty, + RequireDisasterRecovery: props.RequireDisasterRecovery?.ToString() ?? string.Empty, + RequireHighAvailability: props.RequireHighAvailability?.ToString() ?? string.Empty); + + var mappedSystemData = systemData is null + ? null + : new GoalTemplateInfoSystemData( + CreatedAt: systemData.CreatedOn?.ToString("o") ?? string.Empty, + CreatedBy: systemData.CreatedBy ?? string.Empty, + CreatedByType: systemData.CreatedByType?.ToString() ?? string.Empty, + LastModifiedAt: systemData.LastModifiedOn?.ToString("o") ?? string.Empty, + LastModifiedBy: systemData.LastModifiedBy ?? string.Empty, + LastModifiedByType: systemData.LastModifiedByType?.ToString() ?? string.Empty); + + return new GoalTemplateInfo( + Id: data.Id?.ToString() ?? string.Empty, + Name: data.Name ?? string.Empty, + Properties: mappedProperties, + SystemData: mappedSystemData); + } + + public async Task> ListGoalAssignmentsAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + GoalAssignmentCollection goalAssignments = armClient.GetGoalAssignments(serviceGroupId); + + var result = new List(); + await foreach (var goalAssignment in goalAssignments.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: goalAssignment.Data.Id?.ToString() ?? string.Empty, + Name: goalAssignment.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetGoalAssignmentAsync(string serviceGroup, string goalAssignment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + GoalAssignmentCollection goalAssignments = armClient.GetGoalAssignments(serviceGroupId); + GoalAssignmentResource resource = await goalAssignments.GetAsync(goalAssignment, cancellationToken); + + return MapGoalAssignment(resource.Data); + } + + private static GoalAssignmentInfo MapGoalAssignment(GoalAssignmentData data) + { + var props = data.Properties; + var systemData = data.SystemData; + + var mappedProperties = props is null + ? null + : new GoalAssignmentInfoProperties( + GoalAssignmentType: props.GoalAssignmentType.ToString(), + GoalTemplateId: props.GoalTemplateId?.ToString() ?? string.Empty, + ProvisioningState: props.ProvisioningState?.ToString() ?? string.Empty); + + var mappedSystemData = systemData is null + ? null + : new GoalAssignmentInfoSystemData( + CreatedAt: systemData.CreatedOn?.ToString("o") ?? string.Empty, + CreatedBy: systemData.CreatedBy ?? string.Empty, + CreatedByType: systemData.CreatedByType?.ToString() ?? string.Empty, + LastModifiedAt: systemData.LastModifiedOn?.ToString("o") ?? string.Empty, + LastModifiedBy: systemData.LastModifiedBy ?? string.Empty, + LastModifiedByType: systemData.LastModifiedByType?.ToString() ?? string.Empty); + + return new GoalAssignmentInfo( + Id: data.Id?.ToString() ?? string.Empty, + Name: data.Name ?? string.Empty, + Properties: mappedProperties, + SystemData: mappedSystemData); + } + + public async Task> ListUsagePlansAsync(string resourceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var resourceGroupId = new ResourceIdentifier($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}"); + var resourceGroupResource = armClient.GetResourceGroupResource(resourceGroupId); + UsagePlanCollection usagePlans = resourceGroupResource.GetUsagePlans(); + + var result = new List(); + await foreach (var usagePlan in usagePlans.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: usagePlan.Data.Id?.ToString() ?? string.Empty, + Name: usagePlan.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task> ListUsagePlansBySubscriptionAsync(string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var subscriptionId2 = new ResourceIdentifier($"/subscriptions/{subscriptionId}"); + var subscriptionResource = armClient.GetSubscriptionResource(subscriptionId2); + + var result = new List(); + await foreach (var usagePlan in subscriptionResource.GetUsagePlansAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: usagePlan.Data.Id?.ToString() ?? string.Empty, + Name: usagePlan.Data.Name ?? string.Empty)); + } + + return result; + } + + private static UsagePlanInfo MapUsagePlan(UsagePlanData data) + { + var props = data.Properties; + var systemData = data.SystemData; + + var mappedProperties = props is null + ? null + : new UsagePlanInfoProperties( + PlanType: props.PlanType.ToString() ?? string.Empty, + ProvisioningState: props.ProvisioningState?.ToString() ?? string.Empty); + + var mappedSystemData = systemData is null + ? null + : new UsagePlanInfoSystemData( + CreatedAt: systemData.CreatedOn?.ToString("o") ?? string.Empty, + CreatedBy: systemData.CreatedBy ?? string.Empty, + LastModifiedAt: systemData.LastModifiedOn?.ToString("o") ?? string.Empty, + LastModifiedBy: systemData.LastModifiedBy ?? string.Empty); + + return new UsagePlanInfo( + Id: data.Id?.ToString() ?? string.Empty, + Name: data.Name ?? string.Empty, + ResourceType: data.ResourceType.ToString(), + Location: data.Location.Name ?? string.Empty, + Tags: data.Tags is null ? null : new Dictionary(data.Tags), + Properties: mappedProperties, + SystemData: mappedSystemData); + } + + public async Task GetUsagePlanAsync(string resourceGroup, string usagePlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var resourceGroupId = new ResourceIdentifier($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}"); + var resourceGroupResource = armClient.GetResourceGroupResource(resourceGroupId); + UsagePlanCollection usagePlans = resourceGroupResource.GetUsagePlans(); + UsagePlanResource resource = await usagePlans.GetAsync(usagePlan, cancellationToken); + + return MapUsagePlan(resource.Data); + } + + public async Task> ListUsagePlanEnrollmentsAsync(string resourceGroup, string usagePlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var usagePlanId = new ResourceIdentifier($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.AzureResilienceManagement/usagePlans/{usagePlan}"); + var usagePlanResource = armClient.GetUsagePlanResource(usagePlanId); + UsagePlanEnrollmentCollection enrollments = usagePlanResource.GetUsagePlanEnrollments(); + + var result = new List(); + await foreach (var enrollment in enrollments.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: enrollment.Data.Id?.ToString() ?? string.Empty, + Name: enrollment.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetUsagePlanEnrollmentAsync(string resourceGroup, string usagePlan, string enrollment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var usagePlanId = new ResourceIdentifier($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.AzureResilienceManagement/usagePlans/{usagePlan}"); + var usagePlanResource = armClient.GetUsagePlanResource(usagePlanId); + UsagePlanEnrollmentCollection enrollments = usagePlanResource.GetUsagePlanEnrollments(); + UsagePlanEnrollmentResource resource = await enrollments.GetAsync(enrollment, cancellationToken); + + return MapUsagePlanEnrollment(resource.Data); + } + + private static UsagePlanEnrollmentInfo MapUsagePlanEnrollment(UsagePlanEnrollmentData data) + { + var props = data.Properties; + var systemData = data.SystemData; + + var mappedProperties = props is null + ? null + : new UsagePlanEnrollmentInfoProperties( + ServiceGroupId: props.ServiceGroupId?.ToString() ?? string.Empty, + ProvisioningState: props.ProvisioningState?.ToString() ?? string.Empty, + ErrorDetails: props.ErrorDetails is null + ? null + : new UsagePlanEnrollmentInfoErrorDetails( + Code: props.ErrorDetails.Code ?? string.Empty, + Message: props.ErrorDetails.Message ?? string.Empty)); + + var mappedSystemData = systemData is null + ? null + : new UsagePlanEnrollmentInfoSystemData( + CreatedAt: systemData.CreatedOn?.ToString("o") ?? string.Empty, + CreatedBy: systemData.CreatedBy ?? string.Empty, + CreatedByType: systemData.CreatedByType?.ToString() ?? string.Empty, + LastModifiedAt: systemData.LastModifiedOn?.ToString("o") ?? string.Empty, + LastModifiedBy: systemData.LastModifiedBy ?? string.Empty, + LastModifiedByType: systemData.LastModifiedByType?.ToString() ?? string.Empty); + + return new UsagePlanEnrollmentInfo( + Id: data.Id?.ToString() ?? string.Empty, + Name: data.Name ?? string.Empty, + Properties: mappedProperties, + SystemData: mappedSystemData); + } + + public async Task> ListGoalResourcesAsync(string serviceGroup, string goalAssignment, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var goalAssignmentId = GoalAssignmentResource.CreateResourceIdentifier(serviceGroup, goalAssignment); + GoalMembersCollection goalMembers = armClient.GetGoalAssignmentResource(goalAssignmentId).GetAllGoalMembers(); + + var result = new List(); + await foreach (var goalMember in goalMembers.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: goalMember.Data.Id?.ToString() ?? string.Empty, + Name: goalMember.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetGoalResourceAsync(string serviceGroup, string goalAssignment, string goalResource, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var goalMemberId = GoalMembersResource.CreateResourceIdentifier(serviceGroup, goalAssignment, goalResource); + GoalMembersResource resource = await armClient.GetGoalMembersResource(goalMemberId).GetAsync(cancellationToken); + + return MapGoalResource(resource.Data); + } + + private static GoalResourceInfo MapGoalResource(GoalMembersData data) + { + var props = data.Properties; + var systemData = data.SystemData; + + var mappedProperties = props is null + ? null + : new GoalResourceInfoProperties( + DisasterRecoveryAttestationStatus: props.DisasterRecoveryAttestationStatus.ToString() ?? string.Empty, + DisasterRecoveryGoalParticipation: props.DisasterRecoveryGoalParticipation.ToString() ?? string.Empty, + ExclusionReasonForDisasterRecoveryGoals: props.ExclusionReasonForDisasterRecoveryGoals.ToString() ?? string.Empty, + ExclusionReasonForHighAvailabilityGoals: props.ExclusionReasonForHighAvailabilityGoals.ToString() ?? string.Empty, + HighAvailabilityAttestationStatus: props.HighAvailabilityAttestationStatus.ToString() ?? string.Empty, + HighAvailabilityGoalParticipation: props.HighAvailabilityGoalParticipation.ToString() ?? string.Empty, + ProvisioningState: props.ProvisioningState?.ToString() ?? string.Empty, + ResourceArmId: props.ResourceArmId?.ToString() ?? string.Empty, + ServiceGroupMemberships: props.ServiceGroupMemberships is null || props.ServiceGroupMemberships.Count == 0 + ? null + : props.ServiceGroupMemberships + .Select(m => new GoalResourceServiceGroupMembership( + MembershipType: m.MembershipType.ToString() ?? string.Empty, + ServiceGroupId: m.ServiceGroupId?.ToString() ?? string.Empty)) + .ToList()); + + var mappedSystemData = systemData is null + ? null + : new GoalResourceInfoSystemData( + CreatedAt: systemData.CreatedOn?.ToString("o") ?? string.Empty, + CreatedBy: systemData.CreatedBy ?? string.Empty, + CreatedByType: systemData.CreatedByType?.ToString() ?? string.Empty, + LastModifiedAt: systemData.LastModifiedOn?.ToString("o") ?? string.Empty, + LastModifiedBy: systemData.LastModifiedBy ?? string.Empty, + LastModifiedByType: systemData.LastModifiedByType?.ToString() ?? string.Empty); + + return new GoalResourceInfo( + Id: data.Id?.ToString() ?? string.Empty, + Name: data.Name ?? string.Empty, + Properties: mappedProperties, + SystemData: mappedSystemData); + } + + public async Task> ListDrillsAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + ResilienceManagementDrillCollection drills = armClient.GetResilienceManagementDrills(serviceGroupId); + + var result = new List(); + await foreach (var drill in drills.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: drill.Data.Id?.ToString() ?? string.Empty, + Name: drill.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetDrillAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + ResilienceManagementDrillCollection drills = armClient.GetResilienceManagementDrills(serviceGroupId); + Response response = await drills.GetAsync(drill, cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListDrillRunsAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillId = ResilienceManagementDrillResource.CreateResourceIdentifier(serviceGroup, drill); + DrillRunCollection drillRuns = armClient.GetDrillRuns(drillId); + + var result = new List(); + await foreach (var drillRun in drillRuns.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: drillRun.Data.Id?.ToString() ?? string.Empty, + Name: drillRun.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetDrillRunAsync(string serviceGroup, string drill, string drillRun, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillRunId = DrillRunResource.CreateResourceIdentifier(serviceGroup, drill, drillRun); + Response response = await armClient.GetDrillRunResource(drillRunId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListDrillRunResourcesAsync(string serviceGroup, string drill, string drillRun, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillRunId = DrillRunResource.CreateResourceIdentifier(serviceGroup, drill, drillRun); + DrillRunResource drillRunResource = await armClient.GetDrillRunResource(drillRunId).GetAsync(cancellationToken); + DrillRunTargetCollection drillRunTargets = drillRunResource.GetDrillRunTargets(); + + var result = new List(); + await foreach (var drillRunTarget in drillRunTargets.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: drillRunTarget.Data.Id?.ToString() ?? string.Empty, + Name: drillRunTarget.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetDrillRunResourceAsync(string serviceGroup, string drill, string drillRun, string drillRunTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillRunTargetId = DrillRunTargetResource.CreateResourceIdentifier(serviceGroup, drill, drillRun, drillRunTarget); + Response response = await armClient.GetDrillRunTargetResource(drillRunTargetId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListDrillResourcesAsync(string serviceGroup, string drill, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillId = ResilienceManagementDrillResource.CreateResourceIdentifier(serviceGroup, drill); + DrillTargetCollection drillTargets = armClient.GetDrillTargets(drillId); + + var result = new List(); + await foreach (var drillTarget in drillTargets.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: drillTarget.Data.Id?.ToString() ?? string.Empty, + Name: drillTarget.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetDrillResourceAsync(string serviceGroup, string drill, string drillTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var drillTargetId = DrillTargetResource.CreateResourceIdentifier(serviceGroup, drill, drillTarget); + Response response = await armClient.GetDrillTargetResource(drillTargetId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListRecoveryPlansAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); + RecoveryPlanCollection recoveryPlans = armClient.GetRecoveryPlans(serviceGroupId); + + var result = new List(); + await foreach (var recoveryPlan in recoveryPlans.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: recoveryPlan.Data.Id?.ToString() ?? string.Empty, + Name: recoveryPlan.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetRecoveryPlanAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryPlanId = RecoveryPlanResource.CreateResourceIdentifier(serviceGroup, recoveryPlan); + Response response = await armClient.GetRecoveryPlanResource(recoveryPlanId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListRecoveryResourcesAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryPlanId = RecoveryPlanResource.CreateResourceIdentifier(serviceGroup, recoveryPlan); + RecoveryPlanResource recoveryPlanResource = await armClient.GetRecoveryPlanResource(recoveryPlanId).GetAsync(cancellationToken); + RecoveryMembersCollection recoveryMembers = recoveryPlanResource.GetAllRecoveryMembers(); + + var result = new List(); + await foreach (var recoveryMember in recoveryMembers.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: recoveryMember.Data.Id?.ToString() ?? string.Empty, + Name: recoveryMember.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetRecoveryResourceAsync(string serviceGroup, string recoveryPlan, string recoveryResource, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryPlanId = RecoveryPlanResource.CreateResourceIdentifier(serviceGroup, recoveryPlan); + RecoveryPlanResource recoveryPlanResource = await armClient.GetRecoveryPlanResource(recoveryPlanId).GetAsync(cancellationToken); + RecoveryMembersCollection recoveryMembers = recoveryPlanResource.GetAllRecoveryMembers(); + Response response = await recoveryMembers.GetAsync(recoveryResource, cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListRecoveryJobsAsync(string serviceGroup, string recoveryPlan, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryPlanId = RecoveryPlanResource.CreateResourceIdentifier(serviceGroup, recoveryPlan); + RecoveryJobCollection recoveryJobs = armClient.GetRecoveryPlanResource(recoveryPlanId).GetRecoveryJobs(); + + var result = new List(); + await foreach (var recoveryJob in recoveryJobs.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: recoveryJob.Data.Id?.ToString() ?? string.Empty, + Name: recoveryJob.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetRecoveryJobAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryJobId = RecoveryJobResource.CreateResourceIdentifier(serviceGroup, recoveryPlan, recoveryJob); + Response response = await armClient.GetRecoveryJobResource(recoveryJobId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } + + public async Task> ListRecoveryJobResourcesAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryJobId = RecoveryJobResource.CreateResourceIdentifier(serviceGroup, recoveryPlan, recoveryJob); + RecoveryJobResource recoveryJobResource = await armClient.GetRecoveryJobResource(recoveryJobId).GetAsync(cancellationToken); + RecoveryJobTargetCollection recoveryJobTargets = recoveryJobResource.GetRecoveryJobTargets(); + + var result = new List(); + await foreach (var recoveryJobTarget in recoveryJobTargets.GetAllAsync(cancellationToken: cancellationToken)) + { + result.Add(new ResourceSummary( + Id: recoveryJobTarget.Data.Id?.ToString() ?? string.Empty, + Name: recoveryJobTarget.Data.Name ?? string.Empty)); + } + + return result; + } + + public async Task GetRecoveryJobResourceAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string recoveryJobTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) + { + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + + var recoveryJobTargetId = RecoveryJobTargetResource.CreateResourceIdentifier(serviceGroup, recoveryPlan, recoveryJob, recoveryJobTarget); + Response response = await armClient.GetRecoveryJobTargetResource(recoveryJobTargetId).GetAsync(cancellationToken); + + using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); + return document.RootElement.Clone(); + } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs new file mode 100644 index 0000000000..92cc1acc9f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +[assembly: Microsoft.Mcp.Tests.Helpers.ClearEnvironmentVariablesBeforeTest] +[assembly: Xunit.CollectionBehavior(Xunit.CollectionBehavior.CollectionPerAssembly)] diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj new file mode 100644 index 0000000000..a4aa709b0b --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj @@ -0,0 +1,21 @@ + + + true + Exe + true + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs new file mode 100644 index 0000000000..61fe6eb508 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills; + +public sealed class DrillGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --drill d1 --subscription sub", true)] + [InlineData("--service-group sg --subscription sub", false)] // Missing drill + [InlineData("--drill d1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --drill d1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetDrillAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"d1\"}").RootElement.Clone()); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsDrill() + { + // Arrange + Service.GetDrillAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"d1\"}").RootElement.Clone()); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--drill", "d1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillGetCommandResult); + Assert.Equal("d1", result.Drill.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetDrillAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--drill", "d1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs new file mode 100644 index 0000000000..ec41a2b59c --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills; + +public sealed class DrillListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --subscription sub", true)] + [InlineData("--subscription sub", false)] // Missing service group + [InlineData("--service-group sg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListDrillsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "drill1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsDrills() + { + // Arrange + var expected = new List { new("id1", "drill1"), new("id2", "drill2") }; + Service.ListDrillsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillListCommandResult); + Assert.NotNull(result.Drills); + Assert.Equal(expected.Count, result.Drills.Count); + Assert.Equal(expected.Select(d => d.Name), result.Drills.Select(d => d.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListDrillsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs new file mode 100644 index 0000000000..5c0b772197 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; + +public sealed class GoalAssignmentCreateCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string ValidArgs = + "--service-group sg --goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency --subscription sub"; + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData(ValidArgs, true)] + [InlineData("--goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-template gt1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing goal assignment + [InlineData("--service-group sg --goal-assignment ga1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing goal template + [InlineData("--service-group sg --goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.CreateGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalAssignmentInfo("id1", "ga1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsCreatedGoalAssignment() + { + // Arrange + Service.CreateGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalAssignmentInfo("id1", "ga1")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentCreateCommandResult); + Assert.NotNull(result.GoalAssignment); + Assert.Equal("ga1", result.GoalAssignment.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.CreateGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs new file mode 100644 index 0000000000..5d89582af2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; + +public sealed class GoalAssignmentGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", true)] + [InlineData("--service-group sg --subscription sub", false)] // Missing goal assignment + [InlineData("--goal-assignment ga1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-assignment ga1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalAssignmentInfo("id1", "ga1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalAssignment() + { + // Arrange + Service.GetGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalAssignmentInfo("id1", "ga1")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentGetCommandResult); + Assert.NotNull(result.GoalAssignment); + Assert.Equal("ga1", result.GoalAssignment.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetGoalAssignmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs new file mode 100644 index 0000000000..584dd3f53e --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; + +public sealed class GoalAssignmentListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --subscription sub", true)] + [InlineData("--subscription sub", false)] // Missing service group + [InlineData("--service-group sg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListGoalAssignmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "assignment1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalAssignments() + { + // Arrange + var expected = new List { new("id1", "assignment1"), new("id2", "assignment2") }; + Service.ListGoalAssignmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentListCommandResult); + Assert.NotNull(result.GoalAssignments); + Assert.Equal(expected.Count, result.GoalAssignments.Count); + Assert.Equal(expected.Select(a => a.Name), result.GoalAssignments.Select(a => a.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListGoalAssignmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs new file mode 100644 index 0000000000..86c5d37185 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Resources; + +public sealed class GoalResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --goal-assignment ga1 --name gr1 --subscription sub", true)] + [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", false)] // Missing name + [InlineData("--service-group sg --name gr1 --subscription sub", false)] // Missing goal assignment + [InlineData("--goal-assignment ga1 --name gr1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-assignment ga1 --name gr1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetGoalResourceAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalResourceInfo("id1", "gr1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalResource() + { + // Arrange + Service.GetGoalResourceAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalResourceInfo("id1", "gr1")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--name", "gr1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceGetCommandResult); + Assert.NotNull(result.GoalResource); + Assert.Equal("gr1", result.GoalResource.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetGoalResourceAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--name", "gr1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs new file mode 100644 index 0000000000..9c08db79c0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Resources; + +public sealed class GoalResourceListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", true)] + [InlineData("--service-group sg --subscription sub", false)] // Missing goal assignment + [InlineData("--goal-assignment ga1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-assignment ga1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListGoalResourcesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "resource1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalResources() + { + // Arrange + var expected = new List { new("id1", "resource1"), new("id2", "resource2") }; + Service.ListGoalResourcesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceListCommandResult); + Assert.NotNull(result.GoalResources); + Assert.Equal(expected.Count, result.GoalResources.Count); + Assert.Equal(expected.Select(r => r.Name), result.GoalResources.Select(r => r.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListGoalResourcesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs new file mode 100644 index 0000000000..aef7347748 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; + +public sealed class GoalTemplateCreateCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string ValidArgs = + "--service-group sg --goal-template gt1 --goal-type Resiliency " + + "--require-high-availability Required --require-disaster-recovery NotRequired " + + "--regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub"; + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData(ValidArgs, true)] + [InlineData("--goal-template gt1 --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub", false)] // Missing goal template + [InlineData("--service-group sg --goal-template gt1 --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.CreateGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalTemplateInfo("id1", "gt1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsCreatedGoalTemplate() + { + // Arrange + Service.CreateGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalTemplateInfo("id1", "gt1")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateCreateCommandResult); + Assert.NotNull(result.GoalTemplate); + Assert.Equal("gt1", result.GoalTemplate.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.CreateGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs new file mode 100644 index 0000000000..62200ad3bd --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; + +public sealed class GoalTemplateGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --goal-template gt1 --subscription sub", true)] + [InlineData("--service-group sg --subscription sub", false)] // Missing goal template + [InlineData("--goal-template gt1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --goal-template gt1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalTemplateInfo("id1", "gt1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalTemplate() + { + // Arrange + Service.GetGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GoalTemplateInfo("id1", "gt1")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-template", "gt1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateGetCommandResult); + Assert.NotNull(result.GoalTemplate); + Assert.Equal("gt1", result.GoalTemplate.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetGoalTemplateAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-template", "gt1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs new file mode 100644 index 0000000000..284a9381a7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; + +public sealed class GoalTemplateListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --subscription sub", true)] + [InlineData("--subscription sub", false)] // Missing service group + [InlineData("--service-group sg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListGoalTemplatesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "template1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsGoalTemplates() + { + // Arrange + var expected = new List { new("id1", "template1"), new("id2", "template2") }; + Service.ListGoalTemplatesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateListCommandResult); + Assert.NotNull(result.GoalTemplates); + Assert.Equal(expected.Count, result.GoalTemplates.Count); + Assert.Equal(expected.Select(t => t.Name), result.GoalTemplates.Select(t => t.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListGoalTemplatesAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs new file mode 100644 index 0000000000..66ad8e1755 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans; + +public sealed class RecoveryPlanGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --recovery-plan rp1 --subscription sub", true)] + [InlineData("--service-group sg --subscription sub", false)] // Missing recovery plan + [InlineData("--recovery-plan rp1 --subscription sub", false)] // Missing service group + [InlineData("--service-group sg --recovery-plan rp1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetRecoveryPlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"rp1\"}").RootElement.Clone()); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsRecoveryPlan() + { + // Arrange + Service.GetRecoveryPlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"rp1\"}").RootElement.Clone()); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--recovery-plan", "rp1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanGetCommandResult); + Assert.Equal("rp1", result.RecoveryPlan.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetRecoveryPlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--recovery-plan", "rp1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs new file mode 100644 index 0000000000..3b41a2db06 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans; + +public sealed class RecoveryPlanListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--service-group sg --subscription sub", true)] + [InlineData("--subscription sub", false)] // Missing service group + [InlineData("--service-group sg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListRecoveryPlansAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "plan1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsRecoveryPlans() + { + // Arrange + var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; + Service.ListRecoveryPlansAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanListCommandResult); + Assert.NotNull(result.RecoveryPlans); + Assert.Equal(expected.Count, result.RecoveryPlans.Count); + Assert.Equal(expected.Select(p => p.Name), result.RecoveryPlans.Select(p => p.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListRecoveryPlansAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs new file mode 100644 index 0000000000..7e1fc96ca4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs @@ -0,0 +1,480 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Mcp.Tests; +using Microsoft.Mcp.Tests.Client; +using Microsoft.Mcp.Tests.Client.Helpers; +using Microsoft.Mcp.Tests.Generated.Models; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests; + +// Live (recorded) tests for the Resilience Management toolset. +// +// All resource names are derived from Settings.ResourceBaseName (AzureBackup-style), so the only +// requirement before recording is to provision resources whose names match these helpers: +// +// service group -> {ResourceBaseName}-sg (tenant-scoped, create out-of-band) +// goal template -> {ResourceBaseName}-gt +// goal assignment -> {ResourceBaseName}-ga +// drill -> {ResourceBaseName}-drill +// recovery plan -> {ResourceBaseName}-rp +// usage plan -> {ResourceBaseName} (created by test-resources.bicep) +// usage plan (create) -> {ResourceBaseName}-up2 +// enrollment -> {ResourceBaseName}-enr +// +// The service-group family (goal templates/assignments/resources, drills, recovery plans) targets a +// tenant-level Microsoft.Management/serviceGroups resource that the resource-group-scoped +// test-resources.bicep cannot create. Provision that service group and its children out-of-band before +// recording. The "create" commands use CreateOrUpdate semantics, so re-recording with the same names +// is safe. +public class ResilienceManagementCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) + : RecordedCommandTestsBase(output, fixture, liveServerFixture) +{ + private string ServiceGroup => $"{Settings.ResourceBaseName}-sg"; + private string GoalTemplate => $"{Settings.ResourceBaseName}-gt"; + private string GoalAssignment => $"{Settings.ResourceBaseName}-ga"; + // A service group can hold only one goal assignment, and ServiceGroup already has GoalAssignment. + // The create-assignment test therefore targets a dedicated empty service group + its own template. + private string GoalAssignmentServiceGroup => $"{Settings.ResourceBaseName}-sg3"; + private string GoalAssignmentTemplate => $"{Settings.ResourceBaseName}-gt3"; + private string GoalAssignmentToCreate => $"{Settings.ResourceBaseName}-ga2"; + private string Drill => $"{Settings.ResourceBaseName}-drill"; + private string RecoveryPlan => $"{Settings.ResourceBaseName}-rp"; + private string UsagePlan => Settings.ResourceBaseName; + private string UsagePlanToCreate => $"{Settings.ResourceBaseName}-up2"; + private string Enrollment => $"{Settings.ResourceBaseName}-enr"; + + // The deploy script names the resource group "{accountName}-{ResourceBaseName}" (e.g. "alias-mcp1234"). + // The default ResourceBaseName sanitizer only replaces the base-name part, leaving the account-name + // prefix (the recording user's alias) in response bodies (resource ids, systemData). Strip that prefix + // so the full resource group name is sanitized everywhere. + private string AccountPrefix + { + get + { + var rg = Settings.ResourceGroupName; + var suffix = $"-{Settings.ResourceBaseName}"; + return rg.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) ? rg[..^suffix.Length] : rg; + } + } + + // Static regex (no Settings needed), so this can use the field-initializer form that the base class + // appends its default ResourceBaseName/SubscriptionId sanitizers to. Scrubs the recording user's UPN + // that the service returns in createdBy/lastModifiedBy for the create commands. + public override List GeneralRegexSanitizers { get; } = + [ + new GeneralRegexSanitizer(new GeneralRegexSanitizerBody() + { + Regex = @"[A-Za-z0-9._%+-]+@microsoft\.com", + Value = "sanitized@example.com", + }), + ]; + + // Computed (depends on Settings, available at sanitizer-apply time). The base class never mutates + // BodyRegexSanitizers, so the computed form is safe here. Matches the account-name prefix, which + // survives the base-name replacement regardless of sanitizer ordering. + public override List BodyRegexSanitizers => + [ + .. base.BodyRegexSanitizers, + new BodyRegexSanitizer(new BodyRegexSanitizerBody() + { + Regex = System.Text.RegularExpressions.Regex.Escape(AccountPrefix), + Value = "Sanitized", + }), + ]; + + // Goal-resource members have server-assigned GUID names. The default name sanitizer rewrites that + // GUID to "Sanitized" in the list response body, but leaves it raw in the goal_resource_get request + // URL. This sanitizer rewrites the goalResources/ URI segment to "Sanitized" as well, so the + // recorded GET URL matches what the test requests during playback (where the list returns "Sanitized"). + public override List UriRegexSanitizers => + [ + .. base.UriRegexSanitizers, + // Replace the entire resourceGroups/ URI segment with "Sanitized". The default base-name + // sanitizer only scrubs the base-name part, leaving the account-name prefix (the recording user's + // alias) as "alias-Sanitized" in request URLs. Matching the whole segment is order-independent and + // ensures record and playback (where ResourceGroupName is "Sanitized") use the same URL. + new UriRegexSanitizer(new UriRegexSanitizerBody + { + Regex = "resource[Gg]roups/(?[^/?]+)", + Value = "Sanitized", + GroupForReplace = "rg", + }), + new UriRegexSanitizer(new UriRegexSanitizerBody + { + Regex = "goalResources/(?[^/?]+)", + Value = "Sanitized", + GroupForReplace = "member", + }), + // Long-running create operations poll operationStatuses URLs that carry rotating signed query + // params (t=&c=&s=...&h=...). These differ per poll and break playback matching. + // Strip everything after the api-version value so every poll collapses to one identical URL. + new UriRegexSanitizer(new UriRegexSanitizerBody + { + Regex = "operationStatuses/[^?]+\\?api-version=[^&]+(?&.*)$", + Value = "", + GroupForReplace = "sig", + }), + ]; + + // --------------------------------------------------------------------------------------------- + // Usage plans (resource-group scoped; provisioned by test-resources.bicep) + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_usage_plans_by_subscription() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_list", + new() + { + { "subscription", Settings.SubscriptionId } + }); + + var usagePlans = result.AssertProperty("usagePlans"); + Assert.Equal(JsonValueKind.Array, usagePlans.ValueKind); + } + + [Fact] + public async Task Should_list_usage_plans_by_resource_group() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName } + }); + + var usagePlans = result.AssertProperty("usagePlans"); + Assert.Equal(JsonValueKind.Array, usagePlans.ValueKind); + } + + [Fact] + public async Task Should_get_usage_plan() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "usage-plan", UsagePlan } + }); + + var usagePlan = result.AssertProperty("usagePlan"); + Assert.Equal(JsonValueKind.Object, usagePlan.ValueKind); + // A default $..name sanitizer replaces the name with "Sanitized" in playback recordings. + Assert.Equal(TestMode == Microsoft.Mcp.Tests.Helpers.TestMode.Playback ? "Sanitized" : UsagePlan, usagePlan.GetProperty("name").GetString()); + } + + [Fact] + public async Task Should_create_usage_plan() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "usage-plan", UsagePlanToCreate }, + { "plan-type", "Basic" } + }); + + var usagePlan = result.AssertProperty("usagePlan"); + Assert.Equal(JsonValueKind.Object, usagePlan.ValueKind); + // A default $..name sanitizer replaces the name with "Sanitized" in playback recordings. + Assert.Equal(TestMode == Microsoft.Mcp.Tests.Helpers.TestMode.Playback ? "Sanitized" : UsagePlanToCreate, usagePlan.GetProperty("name").GetString()); + } + + // --------------------------------------------------------------------------------------------- + // Usage plan enrollments + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_usage_plan_enrollments() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_enrollment_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "usage-plan", UsagePlan } + }); + + var enrollments = result.AssertProperty("enrollments"); + Assert.Equal(JsonValueKind.Array, enrollments.ValueKind); + } + + [Fact] + public async Task Should_get_usage_plan_enrollment() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_enrollment_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "usage-plan", UsagePlan }, + { "enrollment", Enrollment } + }); + + var enrollment = result.AssertProperty("enrollment"); + Assert.Equal(JsonValueKind.Object, enrollment.ValueKind); + } + + [Fact] + public async Task Should_create_usage_plan_enrollment() + { + var result = await CallToolAsync( + "resiliencemanagement_usageplan_enrollment_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "usage-plan", UsagePlan }, + { "enrollment", Enrollment }, + { "service-group", ServiceGroup } + }); + + var enrollment = result.AssertProperty("enrollment"); + Assert.Equal(JsonValueKind.Object, enrollment.ValueKind); + } + + // --------------------------------------------------------------------------------------------- + // Goal templates (service-group scoped; service group created out-of-band) + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_goal_templates() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_template_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup } + }); + + var goalTemplates = result.AssertProperty("goalTemplates"); + Assert.Equal(JsonValueKind.Array, goalTemplates.ValueKind); + } + + [Fact] + public async Task Should_get_goal_template() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_template_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-template", GoalTemplate } + }); + + var goalTemplate = result.AssertProperty("goalTemplate"); + Assert.Equal(JsonValueKind.Object, goalTemplate.ValueKind); + } + + [Fact] + public async Task Should_create_goal_template() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_template_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-template", GoalTemplate }, + { "goal-type", "Resiliency" }, + { "require-high-availability", "Required" }, + { "require-disaster-recovery", "NotRequired" }, + { "regional-recovery-point-objective", "PT15M" }, + { "regional-recovery-time-objective", "PT30M" } + }); + + var goalTemplate = result.AssertProperty("goalTemplate"); + Assert.Equal(JsonValueKind.Object, goalTemplate.ValueKind); + } + + // --------------------------------------------------------------------------------------------- + // Goal assignments + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_goal_assignments() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_assignment_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup } + }); + + var goalAssignments = result.AssertProperty("goalAssignments"); + Assert.Equal(JsonValueKind.Array, goalAssignments.ValueKind); + } + + [Fact] + public async Task Should_get_goal_assignment() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_assignment_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-assignment", GoalAssignment } + }); + + var goalAssignment = result.AssertProperty("goalAssignment"); + Assert.Equal(JsonValueKind.Object, goalAssignment.ValueKind); + } + + [Fact] + public async Task Should_create_goal_assignment() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_assignment_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", GoalAssignmentServiceGroup }, + { "goal-assignment", GoalAssignmentToCreate }, + { "goal-template", GoalAssignmentTemplate }, + { "goal-assignment-type", "Resiliency" } + }); + + var goalAssignment = result.AssertProperty("goalAssignment"); + Assert.Equal(JsonValueKind.Object, goalAssignment.ValueKind); + } + + // --------------------------------------------------------------------------------------------- + // Goal resources (members of a goal assignment) + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_goal_resources() + { + var result = await CallToolAsync( + "resiliencemanagement_goal_resource_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-assignment", GoalAssignment } + }); + + var goalResources = result.AssertProperty("goalResources"); + Assert.Equal(JsonValueKind.Array, goalResources.ValueKind); + } + + [Fact] + public async Task Should_get_goal_resource() + { + // The goal resource name is a member of the assignment, discovered from the list call. + var listResult = await CallToolAsync( + "resiliencemanagement_goal_resource_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-assignment", GoalAssignment } + }); + + var goalResources = listResult.AssertProperty("goalResources"); + Assert.Equal(JsonValueKind.Array, goalResources.ValueKind); + + var first = goalResources.EnumerateArray().FirstOrDefault(); + Assert.Equal(JsonValueKind.Object, first.ValueKind); + var name = first.GetProperty("name").GetString(); + Assert.NotNull(name); + + var result = await CallToolAsync( + "resiliencemanagement_goal_resource_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "goal-assignment", GoalAssignment }, + { "name", name! } + }); + + var goalResource = result.AssertProperty("goalResource"); + Assert.Equal(JsonValueKind.Object, goalResource.ValueKind); + } + + // --------------------------------------------------------------------------------------------- + // Drills + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_drills() + { + var result = await CallToolAsync( + "resiliencemanagement_drill_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup } + }); + + var drills = result.AssertProperty("drills"); + Assert.Equal(JsonValueKind.Array, drills.ValueKind); + } + + [Fact] + public async Task Should_get_drill() + { + var result = await CallToolAsync( + "resiliencemanagement_drill_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "drill", Drill } + }); + + var drill = result.AssertProperty("drill"); + Assert.Equal(JsonValueKind.Object, drill.ValueKind); + } + + // --------------------------------------------------------------------------------------------- + // Recovery plans + // --------------------------------------------------------------------------------------------- + + [Fact] + public async Task Should_list_recovery_plans() + { + var result = await CallToolAsync( + "resiliencemanagement_recovery_plan_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup } + }); + + var recoveryPlans = result.AssertProperty("recoveryPlans"); + Assert.Equal(JsonValueKind.Array, recoveryPlans.ValueKind); + } + + [Fact] + public async Task Should_get_recovery_plan() + { + var result = await CallToolAsync( + "resiliencemanagement_recovery_plan_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", ServiceGroup }, + { "recovery-plan", RecoveryPlan } + }); + + var recoveryPlan = result.AssertProperty("recoveryPlan"); + Assert.Equal(JsonValueKind.Object, recoveryPlan.ValueKind); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs new file mode 100644 index 0000000000..f96d092b37 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; + +public sealed class UsagePlanEnrollmentCreateCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string ValidArgs = + "--resource-group rg --usage-plan up1 --enrollment e1 --service-group sg --subscription sub"; + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData(ValidArgs, true)] + [InlineData("--usage-plan up1 --enrollment e1 --service-group sg --subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg --enrollment e1 --service-group sg --subscription sub", false)] // Missing usage plan + [InlineData("--resource-group rg --usage-plan up1 --service-group sg --subscription sub", false)] // Missing enrollment + [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --subscription sub", false)] // Missing service group + [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --service-group sg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.CreateUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsCreatedEnrollment() + { + // Arrange + Service.CreateUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentCreateCommandResult); + Assert.NotNull(result.Enrollment); + Assert.Equal("e1", result.Enrollment.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.CreateUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs new file mode 100644 index 0000000000..677a39c157 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; + +public sealed class UsagePlanEnrollmentGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --subscription sub", true)] + [InlineData("--resource-group rg --usage-plan up1 --subscription sub", false)] // Missing enrollment + [InlineData("--resource-group rg --enrollment e1 --subscription sub", false)] // Missing usage plan + [InlineData("--usage-plan up1 --enrollment e1 --subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg --usage-plan up1 --enrollment e1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsEnrollment() + { + // Arrange + Service.GetUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--enrollment", "e1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentGetCommandResult); + Assert.NotNull(result.Enrollment); + Assert.Equal("e1", result.Enrollment.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetUsagePlanEnrollmentAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--enrollment", "e1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs new file mode 100644 index 0000000000..3b1b342284 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; + +public sealed class UsagePlanEnrollmentListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--resource-group rg --usage-plan up1 --subscription sub", true)] + [InlineData("--resource-group rg --subscription sub", false)] // Missing usage plan + [InlineData("--usage-plan up1 --subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg --usage-plan up1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.ListUsagePlanEnrollmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "enrollment1") }); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsEnrollments() + { + // Arrange + var expected = new List { new("id1", "enrollment1"), new("id2", "enrollment2") }; + Service.ListUsagePlanEnrollmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentListCommandResult); + Assert.NotNull(result.Enrollments); + Assert.Equal(expected.Count, result.Enrollments.Count); + Assert.Equal(expected.Select(e => e.Name), result.Enrollments.Select(e => e.Name)); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListUsagePlanEnrollmentsAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs new file mode 100644 index 0000000000..1834038646 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; + +public sealed class UsagePlanCreateCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string ValidArgs = "--resource-group rg --usage-plan up1 --plan-type Basic --subscription sub"; + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData(ValidArgs, true)] + [InlineData("--usage-plan up1 --plan-type Basic --subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg --plan-type Basic --subscription sub", false)] // Missing usage plan + [InlineData("--resource-group rg --usage-plan up1 --plan-type Basic", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.CreateUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsCreatedUsagePlan() + { + // Arrange + Service.CreateUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanCreateCommandResult); + Assert.NotNull(result.UsagePlan); + Assert.Equal("up1", result.UsagePlan.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesUsagePlanAlreadyExists() + { + // Arrange + Service.CreateUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.Conflict, "Usage plan name already exists")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + Assert.Equal(HttpStatusCode.Conflict, response.Status); + Assert.Contains("already exists", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.CreateUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync(ValidArgs); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs new file mode 100644 index 0000000000..390fa661ce --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; + +public sealed class UsagePlanGetCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--resource-group rg --usage-plan up1 --subscription sub", true)] + [InlineData("--resource-group rg --subscription sub", false)] // Missing usage plan + [InlineData("--usage-plan up1 --subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg --usage-plan up1", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + Service.GetUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); + } + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsUsagePlan() + { + // Arrange + Service.GetUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); + Assert.NotNull(result.UsagePlan); + Assert.Equal("up1", result.UsagePlan.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.GetUsagePlanAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs new file mode 100644 index 0000000000..ec2ca7fb97 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; + +public sealed class UsagePlanListCommandTests : SubscriptionCommandUnitTestsBase +{ + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = Command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--subscription sub", true)] // Resource group is optional + [InlineData("--subscription sub --resource-group rg", true)] + [InlineData("--resource-group rg", false)] // Missing subscription + [InlineData("", false)] // No parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + Service.ListUsagePlansBySubscriptionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "plan1") }); + Service.ListUsagePlansAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new List { new("id1", "plan1") }); + + // Act + var response = await ExecuteCommandAsync(args); + + // Assert + Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_NoResourceGroup_ListsBySubscription() + { + // Arrange + var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; + Service.ListUsagePlansBySubscriptionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanListCommandResult); + Assert.Equal(expected.Count, result.UsagePlans.Count); + await Service.Received(1).ListUsagePlansBySubscriptionAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_WithResourceGroup_ListsByResourceGroup() + { + // Arrange + var expected = new List { new("id1", "plan1") }; + Service.ListUsagePlansAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expected); + + // Act + var response = await ExecuteCommandAsync("--resource-group", "rg", "--subscription", "sub"); + + // Assert + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanListCommandResult); + Assert.Equal(expected.Count, result.UsagePlans.Count); + await Service.Received(1).ListUsagePlansAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + Service.ListUsagePlansBySubscriptionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + // Act + var response = await ExecuteCommandAsync("--subscription", "sub"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith("Test error", response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json new file mode 100644 index 0000000000..f9ac2f6d31 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.ResilienceManagement.Tests", + "Tag": "" +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 new file mode 100644 index 0000000000..dab4b7f6d8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 @@ -0,0 +1,19 @@ +param( + [string] $TenantId, + [string] $TestApplicationId, + [string] $ResourceGroupName, + [string] $BaseName, + [hashtable] $DeploymentOutputs, + [hashtable] $AdditionalParameters +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/../../../eng/common/scripts/common.ps1" +. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1" + +# Writes the .testsettings.json consumed by the recorded test harness (subscription, resource group, +# base resource name, etc.). +$testSettings = New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot + +Write-Host "Resilience Management test resources deployed. Usage plan: $($testSettings.ResourceBaseName)" -ForegroundColor Green diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep new file mode 100644 index 0000000000..4c66626699 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep @@ -0,0 +1,53 @@ +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(24) +@description('The base resource name.') +param baseName string = resourceGroup().name + +@description('The location of the resource. By default, this is the same as the resource group.') +param location string = resourceGroup().location + +@description('The tenant ID to which the application and resources belong.') +param tenantId string = '72f988bf-86f1-41af-91ab-2d7cd011db47' + +@description('The client OID to grant access to test resources.') +param testApplicationOid string + +// Usage plans are the only Resilience Management resource that is resource-group scoped and therefore +// provisionable here. They are global resources under the Microsoft.AzureResilienceManagement provider. +resource usagePlan 'Microsoft.AzureResilienceManagement/usagePlans@2026-04-01-preview' = { + name: baseName + location: 'global' + properties: { + planType: 'Standard' + } +} + +// Reader role so the test application identity can list/get the usage plan during recording. +resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: subscription() + // Reader - View all resources, but does not allow you to make any changes. + // See https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#reader + name: 'acdd72a7-3385-48ef-bd42-f606fba81ae7' +} + +// principalType is intentionally omitted so the assignment works whether testApplicationOid is a User +// (local deployments) or a ServicePrincipal (CI). Both are existing principals, so ARM does not need +// the type hint to avoid AAD replication races. +resource appReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(readerRoleDefinition.id, testApplicationOid, resourceGroup().id) + scope: resourceGroup() + properties: { + principalId: testApplicationOid + roleDefinitionId: readerRoleDefinition.id + } +} + +// The goal, drill, and recovery commands operate against a tenant-level +// Microsoft.Management/serviceGroups hierarchy that cannot be created from a resource-group-scoped +// deployment. Provision the service group and its goal templates out-of-band (or in a separate +// tenant-scoped deployment) and surface its name via test-resources-post.ps1 so the live tests can +// reference it through Settings.ResourceBaseName. + +output usagePlanName string = usagePlan.name From 0ef41b30d7a340cad2905e3b8204ea30947dc89c Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 13:51:41 +0530 Subject: [PATCH 02/13] removed outdated test files --- Microsoft.Mcp.slnx | 4 +- .../AssemblyAttributes.cs | 5 - ...cp.Tools.ResilienceManagement.Tests.csproj | 21 - .../Drills/DrillGetCommandTests.cs | 101 ---- .../Drills/DrillListCommandTests.cs | 100 ---- .../GoalAssignmentCreateCommandTests.cs | 112 ---- .../GoalAssignmentGetCommandTests.cs | 102 ---- .../GoalAssignmentListCommandTests.cs | 100 ---- .../Resources/GoalResourceGetCommandTests.cs | 106 ---- .../Resources/GoalResourceListCommandTests.cs | 104 ---- .../GoalTemplateCreateCommandTests.cs | 122 ----- .../Templates/GoalTemplateGetCommandTests.cs | 102 ---- .../Templates/GoalTemplateListCommandTests.cs | 100 ---- .../Plans/RecoveryPlanGetCommandTests.cs | 101 ---- .../Plans/RecoveryPlanListCommandTests.cs | 100 ---- .../ResilienceManagementCommandTests.cs | 480 ------------------ .../UsagePlanEnrollmentCreateCommandTests.cs | 113 ----- .../UsagePlanEnrollmentGetCommandTests.cs | 106 ---- .../UsagePlanEnrollmentListCommandTests.cs | 104 ---- .../UsagePlans/UsagePlanCreateCommandTests.cs | 129 ----- .../UsagePlans/UsagePlanGetCommandTests.cs | 102 ---- .../UsagePlans/UsagePlanListCommandTests.cs | 124 ----- .../assets.json | 6 - .../tests/test-resources-post.ps1 | 19 - .../tests/test-resources.bicep | 53 -- 25 files changed, 1 insertion(+), 2515 deletions(-) delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 68a606aa88..29b112b1ea 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -376,9 +376,7 @@ - - - + diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs deleted file mode 100644 index 92cc1acc9f..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/AssemblyAttributes.cs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -[assembly: Microsoft.Mcp.Tests.Helpers.ClearEnvironmentVariablesBeforeTest] -[assembly: Xunit.CollectionBehavior(Xunit.CollectionBehavior.CollectionPerAssembly)] diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj deleted file mode 100644 index a4aa709b0b..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - true - Exe - true - true - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs deleted file mode 100644 index 61fe6eb508..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using System.Text.Json; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills; - -public sealed class DrillGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --drill d1 --subscription sub", true)] - [InlineData("--service-group sg --subscription sub", false)] // Missing drill - [InlineData("--drill d1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --drill d1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetDrillAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"d1\"}").RootElement.Clone()); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsDrill() - { - // Arrange - Service.GetDrillAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"d1\"}").RootElement.Clone()); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--drill", "d1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillGetCommandResult); - Assert.Equal("d1", result.Drill.GetProperty("name").GetString()); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetDrillAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--drill", "d1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs deleted file mode 100644 index ec41a2b59c..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillListCommandTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills; - -public sealed class DrillListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --subscription sub", true)] - [InlineData("--subscription sub", false)] // Missing service group - [InlineData("--service-group sg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListDrillsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "drill1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsDrills() - { - // Arrange - var expected = new List { new("id1", "drill1"), new("id2", "drill2") }; - Service.ListDrillsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillListCommandResult); - Assert.NotNull(result.Drills); - Assert.Equal(expected.Count, result.Drills.Count); - Assert.Equal(expected.Select(d => d.Name), result.Drills.Select(d => d.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListDrillsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs deleted file mode 100644 index 5c0b772197..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentCreateCommandTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; - -public sealed class GoalAssignmentCreateCommandTests : SubscriptionCommandUnitTestsBase -{ - private const string ValidArgs = - "--service-group sg --goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency --subscription sub"; - - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("create", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData(ValidArgs, true)] - [InlineData("--goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-template gt1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing goal assignment - [InlineData("--service-group sg --goal-assignment ga1 --goal-assignment-type Resiliency --subscription sub", false)] // Missing goal template - [InlineData("--service-group sg --goal-assignment ga1 --goal-template gt1 --goal-assignment-type Resiliency", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.CreateGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalAssignmentInfo("id1", "ga1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsCreatedGoalAssignment() - { - // Arrange - Service.CreateGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalAssignmentInfo("id1", "ga1")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentCreateCommandResult); - Assert.NotNull(result.GoalAssignment); - Assert.Equal("ga1", result.GoalAssignment.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.CreateGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs deleted file mode 100644 index 5d89582af2..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; - -public sealed class GoalAssignmentGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", true)] - [InlineData("--service-group sg --subscription sub", false)] // Missing goal assignment - [InlineData("--goal-assignment ga1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-assignment ga1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalAssignmentInfo("id1", "ga1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalAssignment() - { - // Arrange - Service.GetGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalAssignmentInfo("id1", "ga1")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentGetCommandResult); - Assert.NotNull(result.GoalAssignment); - Assert.Equal("ga1", result.GoalAssignment.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetGoalAssignmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs deleted file mode 100644 index 584dd3f53e..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentListCommandTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; - -public sealed class GoalAssignmentListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --subscription sub", true)] - [InlineData("--subscription sub", false)] // Missing service group - [InlineData("--service-group sg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListGoalAssignmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "assignment1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalAssignments() - { - // Arrange - var expected = new List { new("id1", "assignment1"), new("id2", "assignment2") }; - Service.ListGoalAssignmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentListCommandResult); - Assert.NotNull(result.GoalAssignments); - Assert.Equal(expected.Count, result.GoalAssignments.Count); - Assert.Equal(expected.Select(a => a.Name), result.GoalAssignments.Select(a => a.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListGoalAssignmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs deleted file mode 100644 index 86c5d37185..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Resources; - -public sealed class GoalResourceGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --goal-assignment ga1 --name gr1 --subscription sub", true)] - [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", false)] // Missing name - [InlineData("--service-group sg --name gr1 --subscription sub", false)] // Missing goal assignment - [InlineData("--goal-assignment ga1 --name gr1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-assignment ga1 --name gr1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetGoalResourceAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalResourceInfo("id1", "gr1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalResource() - { - // Arrange - Service.GetGoalResourceAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalResourceInfo("id1", "gr1")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--name", "gr1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceGetCommandResult); - Assert.NotNull(result.GoalResource); - Assert.Equal("gr1", result.GoalResource.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetGoalResourceAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--name", "gr1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs deleted file mode 100644 index 9c08db79c0..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceListCommandTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Resources; - -public sealed class GoalResourceListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --goal-assignment ga1 --subscription sub", true)] - [InlineData("--service-group sg --subscription sub", false)] // Missing goal assignment - [InlineData("--goal-assignment ga1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-assignment ga1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListGoalResourcesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "resource1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalResources() - { - // Arrange - var expected = new List { new("id1", "resource1"), new("id2", "resource2") }; - Service.ListGoalResourcesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceListCommandResult); - Assert.NotNull(result.GoalResources); - Assert.Equal(expected.Count, result.GoalResources.Count); - Assert.Equal(expected.Select(r => r.Name), result.GoalResources.Select(r => r.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListGoalResourcesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-assignment", "ga1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs deleted file mode 100644 index aef7347748..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateCreateCommandTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; - -public sealed class GoalTemplateCreateCommandTests : SubscriptionCommandUnitTestsBase -{ - private const string ValidArgs = - "--service-group sg --goal-template gt1 --goal-type Resiliency " + - "--require-high-availability Required --require-disaster-recovery NotRequired " + - "--regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub"; - - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("create", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData(ValidArgs, true)] - [InlineData("--goal-template gt1 --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M --subscription sub", false)] // Missing goal template - [InlineData("--service-group sg --goal-template gt1 --goal-type Resiliency --require-high-availability Required --require-disaster-recovery NotRequired --regional-recovery-point-objective PT15M --regional-recovery-time-objective PT30M", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.CreateGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalTemplateInfo("id1", "gt1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsCreatedGoalTemplate() - { - // Arrange - Service.CreateGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalTemplateInfo("id1", "gt1")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateCreateCommandResult); - Assert.NotNull(result.GoalTemplate); - Assert.Equal("gt1", result.GoalTemplate.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.CreateGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs deleted file mode 100644 index 62200ad3bd..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; - -public sealed class GoalTemplateGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --goal-template gt1 --subscription sub", true)] - [InlineData("--service-group sg --subscription sub", false)] // Missing goal template - [InlineData("--goal-template gt1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --goal-template gt1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalTemplateInfo("id1", "gt1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalTemplate() - { - // Arrange - Service.GetGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new GoalTemplateInfo("id1", "gt1")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-template", "gt1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateGetCommandResult); - Assert.NotNull(result.GoalTemplate); - Assert.Equal("gt1", result.GoalTemplate.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetGoalTemplateAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--goal-template", "gt1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs deleted file mode 100644 index 284a9381a7..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateListCommandTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; - -public sealed class GoalTemplateListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --subscription sub", true)] - [InlineData("--subscription sub", false)] // Missing service group - [InlineData("--service-group sg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListGoalTemplatesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "template1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsGoalTemplates() - { - // Arrange - var expected = new List { new("id1", "template1"), new("id2", "template2") }; - Service.ListGoalTemplatesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateListCommandResult); - Assert.NotNull(result.GoalTemplates); - Assert.Equal(expected.Count, result.GoalTemplates.Count); - Assert.Equal(expected.Select(t => t.Name), result.GoalTemplates.Select(t => t.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListGoalTemplatesAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs deleted file mode 100644 index 66ad8e1755..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using System.Text.Json; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans; - -public sealed class RecoveryPlanGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --recovery-plan rp1 --subscription sub", true)] - [InlineData("--service-group sg --subscription sub", false)] // Missing recovery plan - [InlineData("--recovery-plan rp1 --subscription sub", false)] // Missing service group - [InlineData("--service-group sg --recovery-plan rp1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetRecoveryPlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"rp1\"}").RootElement.Clone()); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsRecoveryPlan() - { - // Arrange - Service.GetRecoveryPlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(JsonDocument.Parse("{\"id\":\"id1\",\"name\":\"rp1\"}").RootElement.Clone()); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--recovery-plan", "rp1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanGetCommandResult); - Assert.Equal("rp1", result.RecoveryPlan.GetProperty("name").GetString()); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetRecoveryPlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--recovery-plan", "rp1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs deleted file mode 100644 index 3b41a2db06..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanListCommandTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans; - -public sealed class RecoveryPlanListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--service-group sg --subscription sub", true)] - [InlineData("--subscription sub", false)] // Missing service group - [InlineData("--service-group sg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListRecoveryPlansAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "plan1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsRecoveryPlans() - { - // Arrange - var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; - Service.ListRecoveryPlansAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanListCommandResult); - Assert.NotNull(result.RecoveryPlans); - Assert.Equal(expected.Count, result.RecoveryPlans.Count); - Assert.Equal(expected.Select(p => p.Name), result.RecoveryPlans.Select(p => p.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListRecoveryPlansAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--service-group", "sg", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs deleted file mode 100644 index 7e1fc96ca4..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json; -using Microsoft.Mcp.Tests; -using Microsoft.Mcp.Tests.Client; -using Microsoft.Mcp.Tests.Client.Helpers; -using Microsoft.Mcp.Tests.Generated.Models; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests; - -// Live (recorded) tests for the Resilience Management toolset. -// -// All resource names are derived from Settings.ResourceBaseName (AzureBackup-style), so the only -// requirement before recording is to provision resources whose names match these helpers: -// -// service group -> {ResourceBaseName}-sg (tenant-scoped, create out-of-band) -// goal template -> {ResourceBaseName}-gt -// goal assignment -> {ResourceBaseName}-ga -// drill -> {ResourceBaseName}-drill -// recovery plan -> {ResourceBaseName}-rp -// usage plan -> {ResourceBaseName} (created by test-resources.bicep) -// usage plan (create) -> {ResourceBaseName}-up2 -// enrollment -> {ResourceBaseName}-enr -// -// The service-group family (goal templates/assignments/resources, drills, recovery plans) targets a -// tenant-level Microsoft.Management/serviceGroups resource that the resource-group-scoped -// test-resources.bicep cannot create. Provision that service group and its children out-of-band before -// recording. The "create" commands use CreateOrUpdate semantics, so re-recording with the same names -// is safe. -public class ResilienceManagementCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) - : RecordedCommandTestsBase(output, fixture, liveServerFixture) -{ - private string ServiceGroup => $"{Settings.ResourceBaseName}-sg"; - private string GoalTemplate => $"{Settings.ResourceBaseName}-gt"; - private string GoalAssignment => $"{Settings.ResourceBaseName}-ga"; - // A service group can hold only one goal assignment, and ServiceGroup already has GoalAssignment. - // The create-assignment test therefore targets a dedicated empty service group + its own template. - private string GoalAssignmentServiceGroup => $"{Settings.ResourceBaseName}-sg3"; - private string GoalAssignmentTemplate => $"{Settings.ResourceBaseName}-gt3"; - private string GoalAssignmentToCreate => $"{Settings.ResourceBaseName}-ga2"; - private string Drill => $"{Settings.ResourceBaseName}-drill"; - private string RecoveryPlan => $"{Settings.ResourceBaseName}-rp"; - private string UsagePlan => Settings.ResourceBaseName; - private string UsagePlanToCreate => $"{Settings.ResourceBaseName}-up2"; - private string Enrollment => $"{Settings.ResourceBaseName}-enr"; - - // The deploy script names the resource group "{accountName}-{ResourceBaseName}" (e.g. "alias-mcp1234"). - // The default ResourceBaseName sanitizer only replaces the base-name part, leaving the account-name - // prefix (the recording user's alias) in response bodies (resource ids, systemData). Strip that prefix - // so the full resource group name is sanitized everywhere. - private string AccountPrefix - { - get - { - var rg = Settings.ResourceGroupName; - var suffix = $"-{Settings.ResourceBaseName}"; - return rg.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) ? rg[..^suffix.Length] : rg; - } - } - - // Static regex (no Settings needed), so this can use the field-initializer form that the base class - // appends its default ResourceBaseName/SubscriptionId sanitizers to. Scrubs the recording user's UPN - // that the service returns in createdBy/lastModifiedBy for the create commands. - public override List GeneralRegexSanitizers { get; } = - [ - new GeneralRegexSanitizer(new GeneralRegexSanitizerBody() - { - Regex = @"[A-Za-z0-9._%+-]+@microsoft\.com", - Value = "sanitized@example.com", - }), - ]; - - // Computed (depends on Settings, available at sanitizer-apply time). The base class never mutates - // BodyRegexSanitizers, so the computed form is safe here. Matches the account-name prefix, which - // survives the base-name replacement regardless of sanitizer ordering. - public override List BodyRegexSanitizers => - [ - .. base.BodyRegexSanitizers, - new BodyRegexSanitizer(new BodyRegexSanitizerBody() - { - Regex = System.Text.RegularExpressions.Regex.Escape(AccountPrefix), - Value = "Sanitized", - }), - ]; - - // Goal-resource members have server-assigned GUID names. The default name sanitizer rewrites that - // GUID to "Sanitized" in the list response body, but leaves it raw in the goal_resource_get request - // URL. This sanitizer rewrites the goalResources/ URI segment to "Sanitized" as well, so the - // recorded GET URL matches what the test requests during playback (where the list returns "Sanitized"). - public override List UriRegexSanitizers => - [ - .. base.UriRegexSanitizers, - // Replace the entire resourceGroups/ URI segment with "Sanitized". The default base-name - // sanitizer only scrubs the base-name part, leaving the account-name prefix (the recording user's - // alias) as "alias-Sanitized" in request URLs. Matching the whole segment is order-independent and - // ensures record and playback (where ResourceGroupName is "Sanitized") use the same URL. - new UriRegexSanitizer(new UriRegexSanitizerBody - { - Regex = "resource[Gg]roups/(?[^/?]+)", - Value = "Sanitized", - GroupForReplace = "rg", - }), - new UriRegexSanitizer(new UriRegexSanitizerBody - { - Regex = "goalResources/(?[^/?]+)", - Value = "Sanitized", - GroupForReplace = "member", - }), - // Long-running create operations poll operationStatuses URLs that carry rotating signed query - // params (t=&c=&s=...&h=...). These differ per poll and break playback matching. - // Strip everything after the api-version value so every poll collapses to one identical URL. - new UriRegexSanitizer(new UriRegexSanitizerBody - { - Regex = "operationStatuses/[^?]+\\?api-version=[^&]+(?&.*)$", - Value = "", - GroupForReplace = "sig", - }), - ]; - - // --------------------------------------------------------------------------------------------- - // Usage plans (resource-group scoped; provisioned by test-resources.bicep) - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_usage_plans_by_subscription() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_list", - new() - { - { "subscription", Settings.SubscriptionId } - }); - - var usagePlans = result.AssertProperty("usagePlans"); - Assert.Equal(JsonValueKind.Array, usagePlans.ValueKind); - } - - [Fact] - public async Task Should_list_usage_plans_by_resource_group() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName } - }); - - var usagePlans = result.AssertProperty("usagePlans"); - Assert.Equal(JsonValueKind.Array, usagePlans.ValueKind); - } - - [Fact] - public async Task Should_get_usage_plan() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "usage-plan", UsagePlan } - }); - - var usagePlan = result.AssertProperty("usagePlan"); - Assert.Equal(JsonValueKind.Object, usagePlan.ValueKind); - // A default $..name sanitizer replaces the name with "Sanitized" in playback recordings. - Assert.Equal(TestMode == Microsoft.Mcp.Tests.Helpers.TestMode.Playback ? "Sanitized" : UsagePlan, usagePlan.GetProperty("name").GetString()); - } - - [Fact] - public async Task Should_create_usage_plan() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "usage-plan", UsagePlanToCreate }, - { "plan-type", "Basic" } - }); - - var usagePlan = result.AssertProperty("usagePlan"); - Assert.Equal(JsonValueKind.Object, usagePlan.ValueKind); - // A default $..name sanitizer replaces the name with "Sanitized" in playback recordings. - Assert.Equal(TestMode == Microsoft.Mcp.Tests.Helpers.TestMode.Playback ? "Sanitized" : UsagePlanToCreate, usagePlan.GetProperty("name").GetString()); - } - - // --------------------------------------------------------------------------------------------- - // Usage plan enrollments - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_usage_plan_enrollments() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_enrollment_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "usage-plan", UsagePlan } - }); - - var enrollments = result.AssertProperty("enrollments"); - Assert.Equal(JsonValueKind.Array, enrollments.ValueKind); - } - - [Fact] - public async Task Should_get_usage_plan_enrollment() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_enrollment_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "usage-plan", UsagePlan }, - { "enrollment", Enrollment } - }); - - var enrollment = result.AssertProperty("enrollment"); - Assert.Equal(JsonValueKind.Object, enrollment.ValueKind); - } - - [Fact] - public async Task Should_create_usage_plan_enrollment() - { - var result = await CallToolAsync( - "resiliencemanagement_usageplan_enrollment_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "usage-plan", UsagePlan }, - { "enrollment", Enrollment }, - { "service-group", ServiceGroup } - }); - - var enrollment = result.AssertProperty("enrollment"); - Assert.Equal(JsonValueKind.Object, enrollment.ValueKind); - } - - // --------------------------------------------------------------------------------------------- - // Goal templates (service-group scoped; service group created out-of-band) - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_goal_templates() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_template_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup } - }); - - var goalTemplates = result.AssertProperty("goalTemplates"); - Assert.Equal(JsonValueKind.Array, goalTemplates.ValueKind); - } - - [Fact] - public async Task Should_get_goal_template() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_template_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-template", GoalTemplate } - }); - - var goalTemplate = result.AssertProperty("goalTemplate"); - Assert.Equal(JsonValueKind.Object, goalTemplate.ValueKind); - } - - [Fact] - public async Task Should_create_goal_template() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_template_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-template", GoalTemplate }, - { "goal-type", "Resiliency" }, - { "require-high-availability", "Required" }, - { "require-disaster-recovery", "NotRequired" }, - { "regional-recovery-point-objective", "PT15M" }, - { "regional-recovery-time-objective", "PT30M" } - }); - - var goalTemplate = result.AssertProperty("goalTemplate"); - Assert.Equal(JsonValueKind.Object, goalTemplate.ValueKind); - } - - // --------------------------------------------------------------------------------------------- - // Goal assignments - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_goal_assignments() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_assignment_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup } - }); - - var goalAssignments = result.AssertProperty("goalAssignments"); - Assert.Equal(JsonValueKind.Array, goalAssignments.ValueKind); - } - - [Fact] - public async Task Should_get_goal_assignment() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_assignment_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-assignment", GoalAssignment } - }); - - var goalAssignment = result.AssertProperty("goalAssignment"); - Assert.Equal(JsonValueKind.Object, goalAssignment.ValueKind); - } - - [Fact] - public async Task Should_create_goal_assignment() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_assignment_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", GoalAssignmentServiceGroup }, - { "goal-assignment", GoalAssignmentToCreate }, - { "goal-template", GoalAssignmentTemplate }, - { "goal-assignment-type", "Resiliency" } - }); - - var goalAssignment = result.AssertProperty("goalAssignment"); - Assert.Equal(JsonValueKind.Object, goalAssignment.ValueKind); - } - - // --------------------------------------------------------------------------------------------- - // Goal resources (members of a goal assignment) - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_goal_resources() - { - var result = await CallToolAsync( - "resiliencemanagement_goal_resource_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-assignment", GoalAssignment } - }); - - var goalResources = result.AssertProperty("goalResources"); - Assert.Equal(JsonValueKind.Array, goalResources.ValueKind); - } - - [Fact] - public async Task Should_get_goal_resource() - { - // The goal resource name is a member of the assignment, discovered from the list call. - var listResult = await CallToolAsync( - "resiliencemanagement_goal_resource_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-assignment", GoalAssignment } - }); - - var goalResources = listResult.AssertProperty("goalResources"); - Assert.Equal(JsonValueKind.Array, goalResources.ValueKind); - - var first = goalResources.EnumerateArray().FirstOrDefault(); - Assert.Equal(JsonValueKind.Object, first.ValueKind); - var name = first.GetProperty("name").GetString(); - Assert.NotNull(name); - - var result = await CallToolAsync( - "resiliencemanagement_goal_resource_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "goal-assignment", GoalAssignment }, - { "name", name! } - }); - - var goalResource = result.AssertProperty("goalResource"); - Assert.Equal(JsonValueKind.Object, goalResource.ValueKind); - } - - // --------------------------------------------------------------------------------------------- - // Drills - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_drills() - { - var result = await CallToolAsync( - "resiliencemanagement_drill_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup } - }); - - var drills = result.AssertProperty("drills"); - Assert.Equal(JsonValueKind.Array, drills.ValueKind); - } - - [Fact] - public async Task Should_get_drill() - { - var result = await CallToolAsync( - "resiliencemanagement_drill_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "drill", Drill } - }); - - var drill = result.AssertProperty("drill"); - Assert.Equal(JsonValueKind.Object, drill.ValueKind); - } - - // --------------------------------------------------------------------------------------------- - // Recovery plans - // --------------------------------------------------------------------------------------------- - - [Fact] - public async Task Should_list_recovery_plans() - { - var result = await CallToolAsync( - "resiliencemanagement_recovery_plan_list", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup } - }); - - var recoveryPlans = result.AssertProperty("recoveryPlans"); - Assert.Equal(JsonValueKind.Array, recoveryPlans.ValueKind); - } - - [Fact] - public async Task Should_get_recovery_plan() - { - var result = await CallToolAsync( - "resiliencemanagement_recovery_plan_get", - new() - { - { "subscription", Settings.SubscriptionId }, - { "service-group", ServiceGroup }, - { "recovery-plan", RecoveryPlan } - }); - - var recoveryPlan = result.AssertProperty("recoveryPlan"); - Assert.Equal(JsonValueKind.Object, recoveryPlan.ValueKind); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs deleted file mode 100644 index f96d092b37..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentCreateCommandTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; - -public sealed class UsagePlanEnrollmentCreateCommandTests : SubscriptionCommandUnitTestsBase -{ - private const string ValidArgs = - "--resource-group rg --usage-plan up1 --enrollment e1 --service-group sg --subscription sub"; - - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("create", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData(ValidArgs, true)] - [InlineData("--usage-plan up1 --enrollment e1 --service-group sg --subscription sub", false)] // Missing resource group - [InlineData("--resource-group rg --enrollment e1 --service-group sg --subscription sub", false)] // Missing usage plan - [InlineData("--resource-group rg --usage-plan up1 --service-group sg --subscription sub", false)] // Missing enrollment - [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --subscription sub", false)] // Missing service group - [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --service-group sg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.CreateUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsCreatedEnrollment() - { - // Arrange - Service.CreateUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentCreateCommandResult); - Assert.NotNull(result.Enrollment); - Assert.Equal("e1", result.Enrollment.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.CreateUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs deleted file mode 100644 index 677a39c157..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; - -public sealed class UsagePlanEnrollmentGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--resource-group rg --usage-plan up1 --enrollment e1 --subscription sub", true)] - [InlineData("--resource-group rg --usage-plan up1 --subscription sub", false)] // Missing enrollment - [InlineData("--resource-group rg --enrollment e1 --subscription sub", false)] // Missing usage plan - [InlineData("--usage-plan up1 --enrollment e1 --subscription sub", false)] // Missing resource group - [InlineData("--resource-group rg --usage-plan up1 --enrollment e1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsEnrollment() - { - // Arrange - Service.GetUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanEnrollmentInfo("id1", "e1")); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--enrollment", "e1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentGetCommandResult); - Assert.NotNull(result.Enrollment); - Assert.Equal("e1", result.Enrollment.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetUsagePlanEnrollmentAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--enrollment", "e1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs deleted file mode 100644 index 3b1b342284..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentListCommandTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; - -public sealed class UsagePlanEnrollmentListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--resource-group rg --usage-plan up1 --subscription sub", true)] - [InlineData("--resource-group rg --subscription sub", false)] // Missing usage plan - [InlineData("--usage-plan up1 --subscription sub", false)] // Missing resource group - [InlineData("--resource-group rg --usage-plan up1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.ListUsagePlanEnrollmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "enrollment1") }); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsEnrollments() - { - // Arrange - var expected = new List { new("id1", "enrollment1"), new("id2", "enrollment2") }; - Service.ListUsagePlanEnrollmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentListCommandResult); - Assert.NotNull(result.Enrollments); - Assert.Equal(expected.Count, result.Enrollments.Count); - Assert.Equal(expected.Select(e => e.Name), result.Enrollments.Select(e => e.Name)); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListUsagePlanEnrollmentsAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs deleted file mode 100644 index 1834038646..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanCreateCommandTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; - -public sealed class UsagePlanCreateCommandTests : SubscriptionCommandUnitTestsBase -{ - private const string ValidArgs = "--resource-group rg --usage-plan up1 --plan-type Basic --subscription sub"; - - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("create", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData(ValidArgs, true)] - [InlineData("--usage-plan up1 --plan-type Basic --subscription sub", false)] // Missing resource group - [InlineData("--resource-group rg --plan-type Basic --subscription sub", false)] // Missing usage plan - [InlineData("--resource-group rg --usage-plan up1 --plan-type Basic", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.CreateUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsCreatedUsagePlan() - { - // Arrange - Service.CreateUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanCreateCommandResult); - Assert.NotNull(result.UsagePlan); - Assert.Equal("up1", result.UsagePlan.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesUsagePlanAlreadyExists() - { - // Arrange - Service.CreateUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.Conflict, "Usage plan name already exists")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - Assert.Equal(HttpStatusCode.Conflict, response.Status); - Assert.Contains("already exists", response.Message); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.CreateUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync(ValidArgs); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs deleted file mode 100644 index 390fa661ce..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; - -public sealed class UsagePlanGetCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("get", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--resource-group rg --usage-plan up1 --subscription sub", true)] - [InlineData("--resource-group rg --subscription sub", false)] // Missing usage plan - [InlineData("--usage-plan up1 --subscription sub", false)] // Missing resource group - [InlineData("--resource-group rg --usage-plan up1", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - if (shouldSucceed) - { - Service.GetUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); - } - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_ReturnsUsagePlan() - { - // Arrange - Service.GetUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new UsagePlanInfo("id1", "up1", "Microsoft.ResilienceManagement/usagePlans", "eastus")); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); - Assert.NotNull(result.UsagePlan); - Assert.Equal("up1", result.UsagePlan.Name); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.GetUsagePlanAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--usage-plan", "up1", "--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs deleted file mode 100644 index ec2ca7fb97..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanListCommandTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using Azure.Mcp.Tests.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands; -using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; -using Azure.Mcp.Tools.ResilienceManagement.Models; -using Azure.Mcp.Tools.ResilienceManagement.Services; -using Microsoft.Mcp.Core.Options; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; - -public sealed class UsagePlanListCommandTests : SubscriptionCommandUnitTestsBase -{ - [Fact] - public void Constructor_InitializesCommandCorrectly() - { - var command = Command.GetCommand(); - Assert.Equal("list", command.Name); - Assert.NotNull(command.Description); - Assert.NotEmpty(command.Description); - } - - [Theory] - [InlineData("--subscription sub", true)] // Resource group is optional - [InlineData("--subscription sub --resource-group rg", true)] - [InlineData("--resource-group rg", false)] // Missing subscription - [InlineData("", false)] // No parameters - public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) - { - // Arrange - Service.ListUsagePlansBySubscriptionAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "plan1") }); - Service.ListUsagePlansAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(new List { new("id1", "plan1") }); - - // Act - var response = await ExecuteCommandAsync(args); - - // Assert - Assert.Equal(shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest, response.Status); - if (!shouldSucceed) - { - Assert.Contains("required", response.Message.ToLower()); - } - } - - [Fact] - public async Task ExecuteAsync_NoResourceGroup_ListsBySubscription() - { - // Arrange - var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; - Service.ListUsagePlansBySubscriptionAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanListCommandResult); - Assert.Equal(expected.Count, result.UsagePlans.Count); - await Service.Received(1).ListUsagePlansBySubscriptionAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_WithResourceGroup_ListsByResourceGroup() - { - // Arrange - var expected = new List { new("id1", "plan1") }; - Service.ListUsagePlansAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .Returns(expected); - - // Act - var response = await ExecuteCommandAsync("--resource-group", "rg", "--subscription", "sub"); - - // Assert - var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanListCommandResult); - Assert.Equal(expected.Count, result.UsagePlans.Count); - await Service.Received(1).ListUsagePlansAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_HandlesServiceErrors() - { - // Arrange - Service.ListUsagePlansBySubscriptionAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()) - .ThrowsAsync(new Exception("Test error")); - - // Act - var response = await ExecuteCommandAsync("--subscription", "sub"); - - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.Status); - Assert.StartsWith("Test error", response.Message); - } -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json deleted file mode 100644 index f9ac2f6d31..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "AssetsRepo": "Azure/azure-sdk-assets", - "AssetsRepoPrefixPath": "", - "TagPrefix": "Azure.Mcp.Tools.ResilienceManagement.Tests", - "Tag": "" -} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 deleted file mode 100644 index dab4b7f6d8..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -param( - [string] $TenantId, - [string] $TestApplicationId, - [string] $ResourceGroupName, - [string] $BaseName, - [hashtable] $DeploymentOutputs, - [hashtable] $AdditionalParameters -) - -$ErrorActionPreference = "Stop" - -. "$PSScriptRoot/../../../eng/common/scripts/common.ps1" -. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1" - -# Writes the .testsettings.json consumed by the recorded test harness (subscription, resource group, -# base resource name, etc.). -$testSettings = New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot - -Write-Host "Resilience Management test resources deployed. Usage plan: $($testSettings.ResourceBaseName)" -ForegroundColor Green diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep deleted file mode 100644 index 4c66626699..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep +++ /dev/null @@ -1,53 +0,0 @@ -targetScope = 'resourceGroup' - -@minLength(3) -@maxLength(24) -@description('The base resource name.') -param baseName string = resourceGroup().name - -@description('The location of the resource. By default, this is the same as the resource group.') -param location string = resourceGroup().location - -@description('The tenant ID to which the application and resources belong.') -param tenantId string = '72f988bf-86f1-41af-91ab-2d7cd011db47' - -@description('The client OID to grant access to test resources.') -param testApplicationOid string - -// Usage plans are the only Resilience Management resource that is resource-group scoped and therefore -// provisionable here. They are global resources under the Microsoft.AzureResilienceManagement provider. -resource usagePlan 'Microsoft.AzureResilienceManagement/usagePlans@2026-04-01-preview' = { - name: baseName - location: 'global' - properties: { - planType: 'Standard' - } -} - -// Reader role so the test application identity can list/get the usage plan during recording. -resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { - scope: subscription() - // Reader - View all resources, but does not allow you to make any changes. - // See https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#reader - name: 'acdd72a7-3385-48ef-bd42-f606fba81ae7' -} - -// principalType is intentionally omitted so the assignment works whether testApplicationOid is a User -// (local deployments) or a ServicePrincipal (CI). Both are existing principals, so ARM does not need -// the type hint to avoid AAD replication races. -resource appReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(readerRoleDefinition.id, testApplicationOid, resourceGroup().id) - scope: resourceGroup() - properties: { - principalId: testApplicationOid - roleDefinitionId: readerRoleDefinition.id - } -} - -// The goal, drill, and recovery commands operate against a tenant-level -// Microsoft.Management/serviceGroups hierarchy that cannot be created from a resource-group-scoped -// deployment. Provision the service group and its goal templates out-of-band (or in a separate -// tenant-scoped deployment) and surface its name via test-resources-post.ps1 so the live tests can -// reference it through Settings.ResourceBaseName. - -output usagePlanName string = usagePlan.name From fb6c4704fa8280327082cb1fc14ace360bba3ffb Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 14:21:46 +0530 Subject: [PATCH 03/13] formatted --- Microsoft.Mcp.slnx | 1 - servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx | 4 ++++ .../Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs | 2 +- .../src/Models/GoalTemplateInfo.cs | 2 +- .../src/Services/IReseilienceManagementService.cs | 2 +- .../src/Services/ResilienceManagementService.cs | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 29b112b1ea..1dd8dcc5ef 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -376,7 +376,6 @@ - diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx index 25ae7510c2..3d92a3b894 100644 --- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx +++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx @@ -326,6 +326,10 @@ + + + + diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs index 9e46d092bc..b41cc886b4 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/GlobalUsings.cs @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -global using System.CommandLine; \ No newline at end of file +global using System.CommandLine; diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs index afbdd0ea08..f75e9a2236 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Models/GoalTemplateInfo.cs @@ -22,4 +22,4 @@ public sealed record GoalTemplateInfoSystemData( [property: JsonPropertyName("createdByType")] string CreatedByType, [property: JsonPropertyName("lastModifiedAt")] string LastModifiedAt, [property: JsonPropertyName("lastModifiedBy")] string LastModifiedBy, - [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); \ No newline at end of file + [property: JsonPropertyName("lastModifiedByType")] string LastModifiedByType); diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs index 3546cb19a7..21f6627f04 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs @@ -63,4 +63,4 @@ public interface IResilienceManagementService Task> ListRecoveryJobResourcesAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task GetRecoveryJobResourceAsync(string serviceGroup, string recoveryPlan, string recoveryJob, string recoveryJobTarget, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs index b30b203602..96e495523a 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs @@ -632,4 +632,4 @@ public async Task GetRecoveryJobResourceAsync(string serviceGroup, using JsonDocument document = JsonDocument.Parse(response.GetRawResponse().Content.ToMemory()); return document.RootElement.Clone(); } -} \ No newline at end of file +} From 88981bbee2e7946113d6449ced6495611fc707e2 Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 15:18:06 +0530 Subject: [PATCH 04/13] updated consolidated-tools.json --- .../Azure.Mcp.Server/docs/azmcp-commands.md | 92 +++++++++++++++++++ .../src/Resources/consolidated-tools.json | 45 +++++++++ 2 files changed, 137 insertions(+) diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index 36d68c404a..58f8df0d87 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -3611,6 +3611,98 @@ azmcp redis create --subscription \ azmcp redis list --subscription ``` +### Azure Resilience Management Operations + +```bash +# Get a resilience goal template, or list all goal templates in a service group (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience goal template get --subscription \ + --service-group \ + [--name ] + +# Get a resilience goal assignment, or list all goal assignments in a service group (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience goal assignment get --subscription \ + --service-group \ + [--name ] + +# Get a resource (member) of a goal assignment, or list all resources of the assignment (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience goal resource get --subscription \ + --service-group \ + --goal-assignment \ + [--name ] + +# Get a resilience usage plan, or list usage plans (omit --name; omit --resource-group to list across the subscription) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience usageplan get --subscription \ + [--resource-group ] \ + [--name ] + +# Get a usage plan enrollment, or list all enrollments of a usage plan (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience usageplan enrollment get --subscription \ + --resource-group \ + --usage-plan \ + [--name ] + +# Get a resilience drill, or list all drills in a service group (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience drill get --subscription \ + --service-group \ + [--name ] + +# Get a resource (target) of a drill, or list all resources of the drill (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience drill resource get --subscription \ + --service-group \ + --drill \ + [--name ] + +# Get a drill run, or list all runs of a drill (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience drill run get --subscription \ + --service-group \ + --drill \ + [--name ] + +# Get a resource (target) of a drill run, or list all resources of the drill run (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience drill run resource get --subscription \ + --service-group \ + --drill \ + --drill-run \ + [--name ] + +# Get a resilience recovery plan, or list all recovery plans in a service group (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience recovery plan get --subscription \ + --service-group \ + [--name ] + +# Get a resource (member) of a recovery plan, or list all resources of the plan (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience recovery plan resource get --subscription \ + --service-group \ + --recovery-plan \ + [--name ] + +# Get a recovery job, or list all recovery jobs of a recovery plan (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience recovery job get --subscription \ + --service-group \ + --recovery-plan \ + [--name ] + +# Get a resource (target) of a recovery job, or list all resources of the job (omit --name) +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp resilience recovery job resource get --subscription \ + --service-group \ + --recovery-plan \ + --recovery-job \ + [--name ] +``` + ### Azure Resource Group Operations ```bash diff --git a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json index 9b0bd58efe..f862dfbc9e 100644 --- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json +++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json @@ -1,5 +1,50 @@ { "consolidated_tools": [ + { + "name": "get_azure_resilience_management_details", + "description": "Get details about Azure Resilience Management resources, including resilience goals (templates, assignments, and their resources), usage plans and enrollments, disaster recovery drills (drills, runs, and their resources), and recovery plans (plans, resources, and jobs) for Azure service groups.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "resilience_goal_template_get", + "resilience_goal_assignment_get", + "resilience_goal_resource_get", + "resilience_usageplan_get", + "resilience_usageplan_enrollment_get", + "resilience_drill_get", + "resilience_drill_resource_get", + "resilience_drill_run_get", + "resilience_drill_run_resource_get", + "resilience_recovery_plan_get", + "resilience_recovery_plan_resource_get", + "resilience_recovery_job_get", + "resilience_recovery_job_resource_get" + ] + }, { "name": "get_azure_subscriptions_and_resource_groups", "description": "Get information about Azure subscriptions, resource groups, and resources within resource groups that the user has access to.", From 1246c7973406341d5b6ede2d6002c9993d06e05e Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 16:32:54 +0530 Subject: [PATCH 05/13] unit tests added --- Microsoft.Mcp.slnx | 3 + ...cp.Tools.ResilienceManagement.Tests.csproj | 20 +++++ .../Drills/DrillGetCommandTests.cs | 65 ++++++++++++++ .../Resources/DrillResourceGetCommandTests.cs | 66 ++++++++++++++ .../Drills/Runs/DrillRunGetCommandTests.cs | 66 ++++++++++++++ .../DrillRunResourceGetCommandTests.cs | 67 +++++++++++++++ .../GoalAssignmentGetCommandTests.cs | 64 ++++++++++++++ .../Resources/GoalResourceGetCommandTests.cs | 65 ++++++++++++++ .../Templates/GoalTemplateGetCommandTests.cs | 64 ++++++++++++++ .../Jobs/RecoveryJobGetCommandTests.cs | 66 ++++++++++++++ .../RecoveryJobResourceGetCommandTests.cs | 67 +++++++++++++++ .../Plans/RecoveryPlanGetCommandTests.cs | 65 ++++++++++++++ .../RecoveryResourceGetCommandTests.cs | 66 ++++++++++++++ .../UsagePlanEnrollmentGetCommandTests.cs | 65 ++++++++++++++ .../UsagePlans/UsagePlanGetCommandTests.cs | 86 +++++++++++++++++++ 15 files changed, 895 insertions(+) create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Resources/DrillResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/DrillRunGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/Resources/DrillRunResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/RecoveryJobGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/Resources/RecoveryJobResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/Resources/RecoveryResourceGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 1dd8dcc5ef..68a606aa88 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -376,6 +376,9 @@ + + + diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj new file mode 100644 index 0000000000..062bb9abbb --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj @@ -0,0 +1,20 @@ + + + true + Exe + false + true + + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs new file mode 100644 index 0000000000..a48eb22ff9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/DrillGetCommandTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills; + +public class DrillGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsDrills_WhenNameOmitted() + { + var expected = new List { new("id1", "drill1"), new("id2", "drill2") }; + Service.ListDrillsAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillGetCommandResult); + Assert.NotNull(result.Drills); + Assert.Equal(2, result.Drills!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsDrill_WhenNameProvided() + { + Service.GetDrillAsync(ServiceGroup, "drill1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("drill1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--name", "drill1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillGetCommandResult); + Assert.Null(result.Drills); + Assert.Equal("drill1", result.Drill.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListDrillsAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Resources/DrillResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Resources/DrillResourceGetCommandTests.cs new file mode 100644 index 0000000000..e21328cef6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Resources/DrillResourceGetCommandTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills.Resources; + +public class DrillResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string Drill = "drill1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsDrillResources_WhenNameOmitted() + { + var expected = new List { new("id1", "target1"), new("id2", "target2") }; + Service.ListDrillResourcesAsync(ServiceGroup, Drill, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillResourceGetCommandResult); + Assert.NotNull(result.DrillResources); + Assert.Equal(2, result.DrillResources!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsDrillResource_WhenNameProvided() + { + Service.GetDrillResourceAsync(ServiceGroup, Drill, "target1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("target1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill, "--name", "target1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillResourceGetCommandResult); + Assert.Null(result.DrillResources); + Assert.Equal("target1", result.DrillResource.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListDrillResourcesAsync(ServiceGroup, Drill, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/DrillRunGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/DrillRunGetCommandTests.cs new file mode 100644 index 0000000000..d0db1eb91c --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/DrillRunGetCommandTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills.Runs; + +public class DrillRunGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string Drill = "drill1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsDrillRuns_WhenNameOmitted() + { + var expected = new List { new("id1", "run1"), new("id2", "run2") }; + Service.ListDrillRunsAsync(ServiceGroup, Drill, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillRunGetCommandResult); + Assert.NotNull(result.DrillRuns); + Assert.Equal(2, result.DrillRuns!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsDrillRun_WhenNameProvided() + { + Service.GetDrillRunAsync(ServiceGroup, Drill, "run1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("run1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill, "--name", "run1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillRunGetCommandResult); + Assert.Null(result.DrillRuns); + Assert.Equal("run1", result.DrillRun.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListDrillRunsAsync(ServiceGroup, Drill, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/Resources/DrillRunResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/Resources/DrillRunResourceGetCommandTests.cs new file mode 100644 index 0000000000..8a2a7a0f6e --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Drills/Runs/Resources/DrillRunResourceGetCommandTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Drills.Runs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Drills.Runs.Resources; + +public class DrillRunResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string Drill = "drill1"; + private const string DrillRun = "run1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsDrillRunResources_WhenNameOmitted() + { + var expected = new List { new("id1", "target1"), new("id2", "target2") }; + Service.ListDrillRunResourcesAsync(ServiceGroup, Drill, DrillRun, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill, "--drill-run", DrillRun); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillRunResourceGetCommandResult); + Assert.NotNull(result.DrillRunResources); + Assert.Equal(2, result.DrillRunResources!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsDrillRunResource_WhenNameProvided() + { + Service.GetDrillRunResourceAsync(ServiceGroup, Drill, DrillRun, "target1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("target1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill, "--drill-run", DrillRun, "--name", "target1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.DrillRunResourceGetCommandResult); + Assert.Null(result.DrillRunResources); + Assert.Equal("target1", result.DrillRunResource.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListDrillRunResourcesAsync(ServiceGroup, Drill, DrillRun, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--drill", Drill, "--drill-run", DrillRun); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs new file mode 100644 index 0000000000..89702bd81e --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Assignments/GoalAssignmentGetCommandTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Assignments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Assignments; + +public class GoalAssignmentGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + + [Fact] + public async Task ExecuteAsync_ListsGoalAssignments_WhenNameOmitted() + { + var expected = new List { new("id1", "assignment1"), new("id2", "assignment2") }; + Service.ListGoalAssignmentsAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentGetCommandResult); + Assert.NotNull(result.GoalAssignments); + Assert.Equal(2, result.GoalAssignments!.Count); + Assert.Null(result.GoalAssignment); + } + + [Fact] + public async Task ExecuteAsync_GetsGoalAssignment_WhenNameProvided() + { + var expected = new GoalAssignmentInfo("id1", "assignment1"); + Service.GetGoalAssignmentAsync(ServiceGroup, "assignment1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--name", "assignment1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalAssignmentGetCommandResult); + Assert.NotNull(result.GoalAssignment); + Assert.Equal("assignment1", result.GoalAssignment!.Name); + Assert.Null(result.GoalAssignments); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListGoalAssignmentsAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs new file mode 100644 index 0000000000..02baa3d3eb --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Resources/GoalResourceGetCommandTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Resources; + +public class GoalResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string GoalAssignment = "assignment1"; + + [Fact] + public async Task ExecuteAsync_ListsGoalResources_WhenNameOmitted() + { + var expected = new List { new("id1", "resource1"), new("id2", "resource2") }; + Service.ListGoalResourcesAsync(ServiceGroup, GoalAssignment, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--goal-assignment", GoalAssignment); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceGetCommandResult); + Assert.NotNull(result.GoalResources); + Assert.Equal(2, result.GoalResources!.Count); + Assert.Null(result.GoalResource); + } + + [Fact] + public async Task ExecuteAsync_GetsGoalResource_WhenNameProvided() + { + var expected = new GoalResourceInfo("id1", "resource1"); + Service.GetGoalResourceAsync(ServiceGroup, GoalAssignment, "resource1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--goal-assignment", GoalAssignment, "--name", "resource1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalResourceGetCommandResult); + Assert.NotNull(result.GoalResource); + Assert.Equal("resource1", result.GoalResource!.Name); + Assert.Null(result.GoalResources); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListGoalResourcesAsync(ServiceGroup, GoalAssignment, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--goal-assignment", GoalAssignment); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs new file mode 100644 index 0000000000..6ad19f0936 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Goals/Templates/GoalTemplateGetCommandTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Goals.Templates; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Goals.Templates; + +public class GoalTemplateGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + + [Fact] + public async Task ExecuteAsync_ListsGoalTemplates_WhenNameOmitted() + { + var expected = new List { new("id1", "template1"), new("id2", "template2") }; + Service.ListGoalTemplatesAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateGetCommandResult); + Assert.NotNull(result.GoalTemplates); + Assert.Equal(2, result.GoalTemplates!.Count); + Assert.Null(result.GoalTemplate); + } + + [Fact] + public async Task ExecuteAsync_GetsGoalTemplate_WhenNameProvided() + { + var expected = new GoalTemplateInfo("id1", "template1"); + Service.GetGoalTemplateAsync(ServiceGroup, "template1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--name", "template1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.GoalTemplateGetCommandResult); + Assert.NotNull(result.GoalTemplate); + Assert.Equal("template1", result.GoalTemplate!.Name); + Assert.Null(result.GoalTemplates); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListGoalTemplatesAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/RecoveryJobGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/RecoveryJobGetCommandTests.cs new file mode 100644 index 0000000000..ea6d357841 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/RecoveryJobGetCommandTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Jobs; + +public class RecoveryJobGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string RecoveryPlan = "plan1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsRecoveryJobs_WhenNameOmitted() + { + var expected = new List { new("id1", "job1"), new("id2", "job2") }; + Service.ListRecoveryJobsAsync(ServiceGroup, RecoveryPlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryJobGetCommandResult); + Assert.NotNull(result.RecoveryJobs); + Assert.Equal(2, result.RecoveryJobs!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsRecoveryJob_WhenNameProvided() + { + Service.GetRecoveryJobAsync(ServiceGroup, RecoveryPlan, "job1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("job1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan, "--name", "job1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryJobGetCommandResult); + Assert.Null(result.RecoveryJobs); + Assert.Equal("job1", result.RecoveryJob.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListRecoveryJobsAsync(ServiceGroup, RecoveryPlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/Resources/RecoveryJobResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/Resources/RecoveryJobResourceGetCommandTests.cs new file mode 100644 index 0000000000..d11df50e11 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Jobs/Resources/RecoveryJobResourceGetCommandTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Jobs.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Jobs.Resources; + +public class RecoveryJobResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string RecoveryPlan = "plan1"; + private const string RecoveryJob = "job1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsRecoveryJobResources_WhenNameOmitted() + { + var expected = new List { new("id1", "target1"), new("id2", "target2") }; + Service.ListRecoveryJobResourcesAsync(ServiceGroup, RecoveryPlan, RecoveryJob, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan, "--recovery-job", RecoveryJob); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryJobResourceGetCommandResult); + Assert.NotNull(result.RecoveryJobResources); + Assert.Equal(2, result.RecoveryJobResources!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsRecoveryJobResource_WhenNameProvided() + { + Service.GetRecoveryJobResourceAsync(ServiceGroup, RecoveryPlan, RecoveryJob, "target1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("target1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan, "--recovery-job", RecoveryJob, "--name", "target1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryJobResourceGetCommandResult); + Assert.Null(result.RecoveryJobResources); + Assert.Equal("target1", result.RecoveryJobResource.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListRecoveryJobResourcesAsync(ServiceGroup, RecoveryPlan, RecoveryJob, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan, "--recovery-job", RecoveryJob); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs new file mode 100644 index 0000000000..0285654647 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/RecoveryPlanGetCommandTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans; + +public class RecoveryPlanGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsRecoveryPlans_WhenNameOmitted() + { + var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; + Service.ListRecoveryPlansAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanGetCommandResult); + Assert.NotNull(result.RecoveryPlans); + Assert.Equal(2, result.RecoveryPlans!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsRecoveryPlan_WhenNameProvided() + { + Service.GetRecoveryPlanAsync(ServiceGroup, "plan1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("plan1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--name", "plan1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryPlanGetCommandResult); + Assert.Null(result.RecoveryPlans); + Assert.Equal("plan1", result.RecoveryPlan.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListRecoveryPlansAsync(ServiceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/Resources/RecoveryResourceGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/Resources/RecoveryResourceGetCommandTests.cs new file mode 100644 index 0000000000..f5a8e86e51 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Recovery/Plans/Resources/RecoveryResourceGetCommandTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.Recovery.Plans.Resources; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.Recovery.Plans.Resources; + +public class RecoveryResourceGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ServiceGroup = "sg1"; + private const string RecoveryPlan = "plan1"; + + private static JsonElement Element(string name) + => JsonDocument.Parse($"{{\"id\":\"id1\",\"name\":\"{name}\"}}").RootElement.Clone(); + + [Fact] + public async Task ExecuteAsync_ListsRecoveryResources_WhenNameOmitted() + { + var expected = new List { new("id1", "member1"), new("id2", "member2") }; + Service.ListRecoveryResourcesAsync(ServiceGroup, RecoveryPlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryResourceGetCommandResult); + Assert.NotNull(result.RecoveryResources); + Assert.Equal(2, result.RecoveryResources!.Count); + } + + [Fact] + public async Task ExecuteAsync_GetsRecoveryResource_WhenNameProvided() + { + Service.GetRecoveryResourceAsync(ServiceGroup, RecoveryPlan, "member1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Element("member1")); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan, "--name", "member1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.RecoveryResourceGetCommandResult); + Assert.Null(result.RecoveryResources); + Assert.Equal("member1", result.RecoveryResource.GetProperty("name").GetString()); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListRecoveryResourcesAsync(ServiceGroup, RecoveryPlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--service-group", ServiceGroup, "--recovery-plan", RecoveryPlan); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs new file mode 100644 index 0000000000..5b26ed79e0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/Enrollments/UsagePlanEnrollmentGetCommandTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans.Enrollments; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans.Enrollments; + +public class UsagePlanEnrollmentGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ResourceGroup = "rg1"; + private const string UsagePlan = "plan1"; + + [Fact] + public async Task ExecuteAsync_ListsEnrollments_WhenNameOmitted() + { + var expected = new List { new("id1", "enrollment1"), new("id2", "enrollment2") }; + Service.ListUsagePlanEnrollmentsAsync(ResourceGroup, UsagePlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--resource-group", ResourceGroup, "--usage-plan", UsagePlan); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentGetCommandResult); + Assert.NotNull(result.Enrollments); + Assert.Equal(2, result.Enrollments!.Count); + Assert.Null(result.Enrollment); + } + + [Fact] + public async Task ExecuteAsync_GetsEnrollment_WhenNameProvided() + { + var expected = new UsagePlanEnrollmentInfo("id1", "enrollment1"); + Service.GetUsagePlanEnrollmentAsync(ResourceGroup, UsagePlan, "enrollment1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--resource-group", ResourceGroup, "--usage-plan", UsagePlan, "--name", "enrollment1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanEnrollmentGetCommandResult); + Assert.NotNull(result.Enrollment); + Assert.Equal("enrollment1", result.Enrollment!.Name); + Assert.Null(result.Enrollments); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListUsagePlanEnrollmentsAsync(ResourceGroup, UsagePlan, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--resource-group", ResourceGroup, "--usage-plan", UsagePlan); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs new file mode 100644 index 0000000000..e7c9dcd791 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/UsagePlans/UsagePlanGetCommandTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands; +using Azure.Mcp.Tools.ResilienceManagement.Commands.UsagePlans; +using Azure.Mcp.Tools.ResilienceManagement.Models; +using Azure.Mcp.Tools.ResilienceManagement.Services; +using Microsoft.Mcp.Core.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests.UsagePlans; + +public class UsagePlanGetCommandTests : SubscriptionCommandUnitTestsBase +{ + private const string SubscriptionId = "00000000-0000-0000-0000-000000000001"; + private const string ResourceGroup = "rg1"; + + [Fact] + public async Task ExecuteAsync_ListsUsagePlansBySubscription_WhenNoResourceGroupOrName() + { + var expected = new List { new("id1", "plan1"), new("id2", "plan2") }; + Service.ListUsagePlansBySubscriptionAsync(SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); + Assert.NotNull(result.UsagePlans); + Assert.Equal(2, result.UsagePlans!.Count); + Assert.Null(result.UsagePlan); + } + + [Fact] + public async Task ExecuteAsync_ListsUsagePlansByResourceGroup_WhenResourceGroupOnly() + { + var expected = new List { new("id1", "plan1") }; + Service.ListUsagePlansAsync(ResourceGroup, SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--resource-group", ResourceGroup); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); + Assert.NotNull(result.UsagePlans); + Assert.Single(result.UsagePlans!); + } + + [Fact] + public async Task ExecuteAsync_GetsUsagePlan_WhenNameAndResourceGroupProvided() + { + var expected = new UsagePlanInfo("id1", "plan1", "Microsoft.AzureResilience/usagePlans", "westus"); + Service.GetUsagePlanAsync(ResourceGroup, "plan1", SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--resource-group", ResourceGroup, "--name", "plan1"); + + var result = ValidateAndDeserializeResponse(response, ResilienceManagementJsonContext.Default.UsagePlanGetCommandResult); + Assert.NotNull(result.UsagePlan); + Assert.Equal("plan1", result.UsagePlan!.Name); + Assert.Null(result.UsagePlans); + } + + [Fact] + public async Task ExecuteAsync_ReturnsError_WhenNameProvidedWithoutResourceGroup() + { + var response = await ExecuteCommandAsync("--subscription", SubscriptionId, "--name", "plan1"); + + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + var expectedError = "Test error"; + Service.ListUsagePlansBySubscriptionAsync(SubscriptionId, Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var response = await ExecuteCommandAsync("--subscription", SubscriptionId); + + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.StartsWith(expectedError, response.Message); + } +} From 1f3cb68ce6c774d5c2e9200b259d5469d302ee4f Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Thu, 25 Jun 2026 16:54:28 +0530 Subject: [PATCH 06/13] formatted --- servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx index 3d92a3b894..dafac9bd08 100644 --- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx +++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx @@ -330,6 +330,9 @@ + + + From 2b7d416ebd1d087508a57d3b04adc64a46abe873 Mon Sep 17 00:00:00 2001 From: Lavish Singal <64147248+LavishSingal@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:09:52 +0530 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/Services/IReseilienceManagementService.cs | 1 - .../src/Services/ResilienceManagementService.cs | 13 ++----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs index 21f6627f04..ac2621da2d 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Text.Json; -using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Tools.ResilienceManagement.Models; using Microsoft.Mcp.Core.Options; diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs index 96e495523a..9791b1102a 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs @@ -3,19 +3,15 @@ using System.Text.Json; using Azure; -using Azure.Core; -using Azure.Core.Pipeline; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; -// using Azure.Mcp.Tools.ResilienceManagement.Commands; using Azure.Mcp.Tools.ResilienceManagement.Models; using Azure.ResourceManager; using Azure.ResourceManager.ResilienceManagement; using Azure.ResourceManager.ResilienceManagement.Models; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Options; -using Microsoft.Mcp.Core.Services.Azure.Authentication; namespace Azure.Mcp.Tools.ResilienceManagement.Services; @@ -27,15 +23,10 @@ public sealed class ResilienceManagementService( { private readonly ISubscriptionService _subscriptionService = subscriptionService; - private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService)); - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + public async Task> ListGoalTemplatesAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) { - var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) - ? subscription - : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; - - ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, retryPolicy: retryPolicy, cancellationToken: cancellationToken); var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); GoalTemplateCollection goalTemplates = armClient.GetGoalTemplates(serviceGroupId); From 905f4ae124f4d0dcda58137e2e73618940987faf Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Fri, 26 Jun 2026 08:56:12 +0530 Subject: [PATCH 08/13] Revert "Apply suggestions from code review" This reverts commit 2b7d416ebd1d087508a57d3b04adc64a46abe873. --- .../src/Services/IReseilienceManagementService.cs | 1 + .../src/Services/ResilienceManagementService.cs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs index ac2621da2d..21f6627f04 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/IReseilienceManagementService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.Json; +using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Tools.ResilienceManagement.Models; using Microsoft.Mcp.Core.Options; diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs index 9791b1102a..96e495523a 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs @@ -3,15 +3,19 @@ using System.Text.Json; using Azure; +using Azure.Core; +using Azure.Core.Pipeline; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; +// using Azure.Mcp.Tools.ResilienceManagement.Commands; using Azure.Mcp.Tools.ResilienceManagement.Models; using Azure.ResourceManager; using Azure.ResourceManager.ResilienceManagement; using Azure.ResourceManager.ResilienceManagement.Models; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Options; +using Microsoft.Mcp.Core.Services.Azure.Authentication; namespace Azure.Mcp.Tools.ResilienceManagement.Services; @@ -23,10 +27,15 @@ public sealed class ResilienceManagementService( { private readonly ISubscriptionService _subscriptionService = subscriptionService; - + private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public async Task> ListGoalTemplatesAsync(string serviceGroup, string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) { - ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, retryPolicy: retryPolicy, cancellationToken: cancellationToken); + var subscriptionId = _subscriptionService.IsSubscriptionId(subscription) + ? subscription + : (await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken)).Data.SubscriptionId; + + ArmClient armClient = await CreateArmClientAsync(tenantIdOrName: tenant, cancellationToken: cancellationToken); var serviceGroupId = new ResourceIdentifier($"/providers/Microsoft.Management/serviceGroups/{serviceGroup}"); GoalTemplateCollection goalTemplates = armClient.GetGoalTemplates(serviceGroupId); From 9dfcf89ee08b92c46ba2bf9170a6dc9546890cb1 Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Sat, 27 Jun 2026 13:59:20 +0530 Subject: [PATCH 09/13] updated e2eTestPrompts and added changelog entry --- .../changelog-entries/1782548815102.yaml | 3 ++ .../Azure.Mcp.Server/docs/e2eTestPrompts.md | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 servers/Azure.Mcp.Server/changelog-entries/1782548815102.yaml diff --git a/servers/Azure.Mcp.Server/changelog-entries/1782548815102.yaml b/servers/Azure.Mcp.Server/changelog-entries/1782548815102.yaml new file mode 100644 index 0000000000..206fbc3ae1 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/1782548815102.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Features Added" + description: "Added the 'resilience' toolset (Azure Resilience Management) with read-only 'get' commands for goals (templates, assignments, resources), usage plans and enrollments, drills (drills, runs, resources), and recovery plans (plans, resources, jobs)." diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index ca609e5301..dcb61546e2 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -854,6 +854,39 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | redis_list | Show me my Redis caches | | redis_list | Get Redis clusters | +## Azure Resilience Management + +| Tool Name | Test Prompt | +|:----------|:----------| +| resilience_drill_get | List all resilience drills in service group | +| resilience_drill_get | Show me the resilience drills in service group | +| resilience_drill_get | Get the details of resilience drill in service group | +| resilience_drill_resource_get | List all resources (targets) of resilience drill in service group | +| resilience_drill_resource_get | Get the resilience drill resource for drill in service group | +| resilience_drill_run_get | List all runs of resilience drill in service group | +| resilience_drill_run_get | Get the details of drill run for drill in service group | +| resilience_drill_run_resource_get | List all resources (targets) of drill run for drill in service group | +| resilience_drill_run_resource_get | Get the drill run resource for drill run of drill in service group | +| resilience_goal_assignment_get | List all resilience goal assignments in service group | +| resilience_goal_assignment_get | Get the details of goal assignment in service group | +| resilience_goal_resource_get | List all resources (members) of goal assignment in service group | +| resilience_goal_resource_get | Get the goal resource for goal assignment in service group | +| resilience_goal_template_get | List all resilience goal templates in service group | +| resilience_goal_template_get | Get the details of goal template in service group | +| resilience_recovery_job_get | List all recovery jobs of recovery plan in service group | +| resilience_recovery_job_get | Get the details of recovery job for recovery plan in service group | +| resilience_recovery_job_resource_get | List all resources (targets) of recovery job for recovery plan in service group | +| resilience_recovery_job_resource_get | Get the recovery job resource for recovery job of recovery plan in service group | +| resilience_recovery_plan_get | List all resilience recovery plans in service group | +| resilience_recovery_plan_get | Get the details of recovery plan in service group | +| resilience_recovery_plan_resource_get | List all resources (members) of recovery plan in service group | +| resilience_recovery_plan_resource_get | Get the recovery resource for recovery plan in service group | +| resilience_usageplan_enrollment_get | List all enrollments of usage plan in resource group | +| resilience_usageplan_enrollment_get | Get the details of usage plan enrollment for usage plan in resource group | +| resilience_usageplan_get | List all resilience usage plans in my subscription | +| resilience_usageplan_get | List all resilience usage plans in resource group | +| resilience_usageplan_get | Get the details of usage plan in resource group | + ## Azure Resource Group | Tool Name | Test Prompt | From 2a67f0c2ccf4383807b49d47846641df11d65070 Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Sat, 27 Jun 2026 14:16:57 +0530 Subject: [PATCH 10/13] removed commented imports --- .../src/Services/ResilienceManagementService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs index 96e495523a..49b350eef3 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/src/Services/ResilienceManagementService.cs @@ -8,7 +8,6 @@ using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; -// using Azure.Mcp.Tools.ResilienceManagement.Commands; using Azure.Mcp.Tools.ResilienceManagement.Models; using Azure.ResourceManager; using Azure.ResourceManager.ResilienceManagement; From d2165742cabcac276b43407eddc7b2e4f53c228a Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Mon, 29 Jun 2026 11:17:35 +0530 Subject: [PATCH 11/13] live tests added --- ...cp.Tools.ResilienceManagement.Tests.csproj | 2 +- .../ResilienceManagementCommandTests.cs | 194 +++++++++++++++ .../assets.json | 6 + .../modules/resource-group-resources.bicep | 69 ++++++ .../modules/subscription-resources.bicep | 50 ++++ .../tests/test-resources-all.bicep | 213 ++++++++++++++++ .../tests/test-resources-post.ps1 | 232 ++++++++++++++++++ .../tests/test-resources.bicep | 64 +++++ 8 files changed, 829 insertions(+), 1 deletion(-) create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 create mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj index 062bb9abbb..28d2eb57c9 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/Azure.Mcp.Tools.ResilienceManagement.Tests.csproj @@ -2,7 +2,7 @@ true Exe - false + true true diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs new file mode 100644 index 0000000000..a25a5db6c9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Mcp.Tests; +using Microsoft.Mcp.Tests.Client; +using Microsoft.Mcp.Tests.Client.Helpers; +using Xunit; + +namespace Azure.Mcp.Tools.ResilienceManagement.Tests; + +/// +/// Live / recorded integration tests for the Resilience Management toolset. +/// Resources are provisioned by test-resources.bicep + test-resources-post.ps1. +/// Drill get tools are intentionally excluded (the drill service is unavailable, so +/// drills are not provisioned). +/// +public class ResilienceManagementCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) + : RecordedCommandTestsBase(output, fixture, liveServerFixture) +{ + [Fact] + public async Task Should_get_usage_plan() + { + var resourceGroupName = RegisterOrRetrieveVariable("resourceGroupName", Settings.ResourceGroupName); + var usagePlanName = RegisterOrRetrieveDeploymentOutputVariable("usagePlanName", "USAGEPLANNAME"); + + var result = await CallToolAsync( + "resilience_usageplan_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", resourceGroupName }, + { "name", usagePlanName } + }); + + var usagePlan = result.AssertProperty("usagePlan"); + Assert.Equal(usagePlanName, usagePlan.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_get_usage_plan_enrollment() + { + var resourceGroupName = RegisterOrRetrieveVariable("resourceGroupName", Settings.ResourceGroupName); + var usagePlanName = RegisterOrRetrieveDeploymentOutputVariable("usagePlanName", "USAGEPLANNAME"); + var enrollmentName = RegisterOrRetrieveDeploymentOutputVariable("enrollmentName", "ENROLLMENTNAME"); + + var result = await CallToolAsync( + "resilience_usageplan_enrollment_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", resourceGroupName }, + { "usage-plan", usagePlanName }, + { "name", enrollmentName } + }); + + var enrollment = result.AssertProperty("enrollment"); + Assert.Equal(enrollmentName, enrollment.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_get_goal_template() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var goalTemplate = RegisterOrRetrieveDeploymentOutputVariable("goalTemplateName", "GOALTEMPLATENAME"); + + var result = await CallToolAsync( + "resilience_goal_template_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "name", goalTemplate } + }); + + var template = result.AssertProperty("goalTemplate"); + Assert.Equal(goalTemplate, template.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_get_goal_assignment() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var goalAssignment = RegisterOrRetrieveDeploymentOutputVariable("goalAssignmentName", "GOALASSIGNMENTNAME"); + + var result = await CallToolAsync( + "resilience_goal_assignment_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "name", goalAssignment } + }); + + var assignment = result.AssertProperty("goalAssignment"); + Assert.Equal(goalAssignment, assignment.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_list_goal_resources() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var goalAssignment = RegisterOrRetrieveDeploymentOutputVariable("goalAssignmentName", "GOALASSIGNMENTNAME"); + + var result = await CallToolAsync( + "resilience_goal_resource_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "goal-assignment", goalAssignment } + }); + + Assert.Equal(JsonValueKind.Array, result.AssertProperty("goalResources").ValueKind); + } + + [Fact] + public async Task Should_get_recovery_plan() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var recoveryPlan = RegisterOrRetrieveDeploymentOutputVariable("recoveryPlanName", "RECOVERYPLANNAME"); + + var result = await CallToolAsync( + "resilience_recovery_plan_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "name", recoveryPlan } + }); + + var plan = result.AssertProperty("recoveryPlan"); + Assert.Equal(recoveryPlan, plan.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_list_recovery_resources() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var recoveryPlan = RegisterOrRetrieveDeploymentOutputVariable("recoveryPlanName", "RECOVERYPLANNAME"); + + var result = await CallToolAsync( + "resilience_recovery_plan_resource_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "recovery-plan", recoveryPlan } + }); + + Assert.Equal(JsonValueKind.Array, result.AssertProperty("recoveryResources").ValueKind); + } + + [Fact] + public async Task Should_get_recovery_job() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var recoveryPlan = RegisterOrRetrieveDeploymentOutputVariable("recoveryPlanName", "RECOVERYPLANNAME"); + var recoveryJob = RegisterOrRetrieveDeploymentOutputVariable("recoveryJobName", "RECOVERYJOBNAME"); + + var result = await CallToolAsync( + "resilience_recovery_job_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "recovery-plan", recoveryPlan }, + { "name", recoveryJob } + }); + + var job = result.AssertProperty("recoveryJob"); + Assert.Equal(recoveryJob, job.AssertProperty("name").GetString()); + } + + [Fact] + public async Task Should_list_recovery_job_resources() + { + var serviceGroup = RegisterOrRetrieveDeploymentOutputVariable("serviceGroupName", "SERVICEGROUPNAME"); + var recoveryPlan = RegisterOrRetrieveDeploymentOutputVariable("recoveryPlanName", "RECOVERYPLANNAME"); + var recoveryJob = RegisterOrRetrieveDeploymentOutputVariable("recoveryJobName", "RECOVERYJOBNAME"); + + var result = await CallToolAsync( + "resilience_recovery_job_resource_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "service-group", serviceGroup }, + { "recovery-plan", recoveryPlan }, + { "recovery-job", recoveryJob } + }); + + Assert.Equal(JsonValueKind.Array, result.AssertProperty("recoveryJobResources").ValueKind); + } +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json new file mode 100644 index 0000000000..fbffe1cca4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.ResilienceManagement.Tests", + "Tag": "" +} diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep new file mode 100644 index 0000000000..2ae92cf1b0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep @@ -0,0 +1,69 @@ +targetScope = 'resourceGroup' + +@description('Azure location for the test resources.') +param location string + +@description('ARM resource ID of the service group these resources belong to.') +param serviceGroupId string + +@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') +param storageAccountName string + +@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') +param usagePlanName string + +@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') +param enrollmentName string + +// Simple ZRS storage account so the resource group has a member resource. +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_ZRS' + } + kind: 'StorageV2' + properties: { + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + supportsHttpsTrafficOnly: true + accessTier: 'Hot' + } +} + +// Add the resource group as a member of the service group. +resource resourceGroupMembership 'Microsoft.Relationships/serviceGroupMember@2023-09-01-preview' = { + name: 'rhub-rg-member' + properties: { + targetId: serviceGroupId + } +} + +// Standard usage plan in the resource group. +resource usagePlan 'Microsoft.AzureResilienceManagement/usagePlans@2026-04-01-preview' = { + name: usagePlanName + location: location + properties: { + planType: 'Standard' + } + tags: { + purpose: 'rhub-live-test' + createdBy: 'bicep' + } + dependsOn: [ + resourceGroupMembership + ] +} + +// Enroll the service group using this usage plan. +resource enrollment 'Microsoft.AzureResilienceManagement/usagePlans/enrollments@2026-04-01-preview' = { + parent: usagePlan + name: enrollmentName + properties: { + serviceGroupId: serviceGroupId + } +} + +output storageAccountId string = storageAccount.id +output usagePlanId string = usagePlan.id +output enrollmentId string = enrollment.id diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep new file mode 100644 index 0000000000..c867aa711f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep @@ -0,0 +1,50 @@ +targetScope = 'subscription' + +@description('Resource group name for live test resources.') +param resourceGroupName string + +@description('Azure location for the test resources.') +param location string + +@description('ARM resource ID of the service group these resources belong to.') +param serviceGroupId string + +@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') +param storageAccountName string + +@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') +param usagePlanName string + +@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') +param enrollmentName string + +// Create the test resource group. +resource testRg 'Microsoft.Resources/resourceGroups@2024-07-01' = { + name: resourceGroupName + location: location + tags: { + purpose: 'rhub-live-test' + createdBy: 'bicep' + } +} + +// Deploy the resource-group-scoped resources into the new resource group. +module rgResources 'resource-group-resources.bicep' = { + name: 'rhub-rg-resources' + scope: resourceGroup(resourceGroupName) + params: { + location: location + serviceGroupId: serviceGroupId + storageAccountName: storageAccountName + usagePlanName: usagePlanName + enrollmentName: enrollmentName + } + dependsOn: [ + testRg + ] +} + +output resourceGroupId string = testRg.id +output storageAccountId string = rgResources.outputs.storageAccountId +output usagePlanId string = rgResources.outputs.usagePlanId +output enrollmentId string = rgResources.outputs.enrollmentId diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep new file mode 100644 index 0000000000..4400c19082 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep @@ -0,0 +1,213 @@ +targetScope = 'tenant' + +// ============================================================================= +// Standalone live-test provisioning template for Azure Resilience Management. +// +// This template is TENANT-SCOPED and creates the resource group itself, so it is +// NOT deployed by the standard MCP test harness (eng/scripts/Deploy-TestResources.ps1 +// only runs New-AzResourceGroupDeployment). Deploy it manually, e.g.: +// +// New-AzTenantDeployment -Location eastus ` +// -TemplateFile tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep ` +// -subscriptionId +// +// Cross-scope resources (the resource group, storage, usage plan, enrollment) are +// created through modules, because Bicep requires modules to deploy to a scope +// other than the file's own scope. The service-group-scoped resources (goal +// template/assignment, recovery plan, drill) are created here at tenant scope. +// +// The deploying identity needs tenant-level serviceGroups/write plus contributor +// on the target subscription. +// ============================================================================= + +@description('Subscription where test RG, storage account, usage plan and enrollment will be created.') +param subscriptionId string + +@description('Azure location for the test resources.') +param location string = 'eastus' + +@description('Short prefix. Keep it lowercase and simple because some resources have strict name limits.') +param prefix string = 'rhubtest' + +@description('Service Group name. Must be unique in tenant. Keep <= 90 chars.') +param serviceGroupName string = '${prefix}-sg-${uniqueString(tenant().tenantId, subscriptionId)}' + +@description('Resource group name for live test resources.') +param resourceGroupName string = '${prefix}-rg-${uniqueString(tenant().tenantId, subscriptionId)}' + +@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') +param storageAccountName string = toLower(take('${prefix}${uniqueString(tenant().tenantId, subscriptionId)}', 24)) + +@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') +param usagePlanName string = take('${prefix}-up-${uniqueString(resourceGroupName)}', 24) + +@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') +param enrollmentName string = take('${prefix}-en-${uniqueString(serviceGroupName)}', 24) + +@description('Goal template name. Must match ^[a-zA-Z0-9-]{3,24}$') +param goalTemplateName string = take('${prefix}-gt-${uniqueString(serviceGroupName)}', 24) + +@description('Goal assignment name. Must match ^[a-zA-Z0-9-]{3,24}$') +param goalAssignmentName string = take('${prefix}-ga-${uniqueString(serviceGroupName)}', 24) + +@description('Drill name. Must match ^[a-zA-Z0-9-]{3,24}$') +param drillName string = take('${prefix}-dr-${uniqueString(serviceGroupName)}', 24) + +@description('Recovery plan name. Must match ^[a-zA-Z0-9-]{3,24}$') +param recoveryPlanName string = take('${prefix}-rp-${uniqueString(serviceGroupName)}', 24) + +// --------------------------------------------------------- +// 1. Create Service Group at tenant scope +// --------------------------------------------------------- +resource serviceGroup 'Microsoft.Management/serviceGroups@2024-02-01-preview' = { + name: serviceGroupName + properties: { + displayName: serviceGroupName + } +} + +// --------------------------------------------------------- +// 2. Resource group + storage + usage plan + enrollment +// (created via a subscription-scoped module because they live in a +// different scope than this tenant-scoped file). +// --------------------------------------------------------- +module subscriptionResources 'modules/subscription-resources.bicep' = { + name: 'rhub-subscription-resources' + scope: subscription(subscriptionId) + params: { + resourceGroupName: resourceGroupName + location: location + serviceGroupId: serviceGroup.id + storageAccountName: storageAccountName + usagePlanName: usagePlanName + enrollmentName: enrollmentName + } +} + +// --------------------------------------------------------- +// 3. Goal Template on the Service Group +// --------------------------------------------------------- +resource goalTemplate 'Microsoft.AzureResilienceManagement/goalTemplates@2026-04-01-preview' = { + scope: serviceGroup + name: goalTemplateName + properties: { + goalType: 'Resiliency' + requireHighAvailability: 'Required' + requireDisasterRecovery: 'NotRequired' + regionalRecoveryPointObjective: 'PT15M' + regionalRecoveryTimeObjective: 'PT30M' + } + dependsOn: [ + subscriptionResources + ] +} + +// --------------------------------------------------------- +// 4. Assign Goal Template to the Service Group +// --------------------------------------------------------- +resource goalAssignment 'Microsoft.AzureResilienceManagement/goalAssignments@2026-04-01-preview' = { + scope: serviceGroup + name: goalAssignmentName + properties: { + goalAssignmentType: 'Resiliency' + goalTemplateId: goalTemplate.id + } +} + +// --------------------------------------------------------- +// 5. Recovery Plan on the Service Group +// --------------------------------------------------------- +resource recoveryPlan 'Microsoft.AzureResilienceManagement/recoveryPlans@2026-04-01-preview' = { + scope: serviceGroup + name: recoveryPlanName + identity: { + type: 'None' + } + properties: { + planDescription: 'Recovery plan for live testing.' + planType: 'Regional' + recoveryGroupsSetting: { + defaultGroup: { + properties: { + description: 'Default recovery group' + groupUniqueId: guid(serviceGroup.id, recoveryPlanName, 'default-group') + orderId: 0 + preActions: [] + postActions: [] + } + } + additionalGroups: [] + } + } + dependsOn: [ + goalAssignment + ] +} + +// --------------------------------------------------------- +// 6. Drill on the Service Group +// --------------------------------------------------------- +resource drill 'Microsoft.AzureResilienceManagement/drills@2026-04-01-preview' = { + scope: serviceGroup + name: drillName + identity: { + type: 'None' + } + properties: { + drillType: 'Regional' + drillAssetProperties: { + subscription: subscriptionId + resourceGroup: resourceGroupName + region: location + } + rbacSetupMode: 'AutomatedCustomRole' + chaosResourceProperties: { + identity: { + type: 'None' + } + chaosResourceIdentityForFaults: { + type: 'None' + } + } + monitoringProperties: { + identity: { + type: 'None' + } + } + recoveryPlanProperties: { + identity: { + type: 'None' + } + } + } + dependsOn: [ + recoveryPlan + ] +} + +// --------------------------------------------------------- +// Outputs (resource IDs) +// --------------------------------------------------------- +output serviceGroupId string = serviceGroup.id +output resourceGroupId string = subscriptionResources.outputs.resourceGroupId +output storageAccountId string = subscriptionResources.outputs.storageAccountId +output usagePlanId string = subscriptionResources.outputs.usagePlanId +output enrollmentId string = subscriptionResources.outputs.enrollmentId +output goalTemplateId string = goalTemplate.id +output goalAssignmentId string = goalAssignment.id +output recoveryPlanId string = recoveryPlan.id +output drillId string = drill.id + +// --------------------------------------------------------- +// Outputs (resource names) — consumed by recorded tests via +// RegisterOrRetrieveDeploymentOutputVariable (deployment output keys are UPPERCASE). +// --------------------------------------------------------- +output serviceGroupName string = serviceGroupName +output resourceGroupName string = resourceGroupName +output storageAccountName string = storageAccountName +output usagePlanName string = usagePlanName +output enrollmentName string = enrollmentName +output goalTemplateName string = goalTemplateName +output goalAssignmentName string = goalAssignmentName +output recoveryPlanName string = recoveryPlanName +output drillName string = drillName diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 new file mode 100644 index 0000000000..4e69831a53 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-post.ps1 @@ -0,0 +1,232 @@ +param( + [string] $TenantId, + [string] $TestApplicationId, + [string] $ResourceGroupName, + [string] $BaseName, + [hashtable] $DeploymentOutputs, + [hashtable] $AdditionalParameters +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/../../../eng/common/scripts/common.ps1" +. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1" + +$testSettings = New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot + +# $testSettings contains: +# - TenantId +# - TenantName +# - SubscriptionId +# - SubscriptionName +# - ResourceGroupName +# - ResourceBaseName + +# $DeploymentOutputs keys are all UPPERCASE + +# The tenant-scoped service group and the usage plan enrollment are created here via +# direct ARM REST calls (Invoke-AzRestMethod) because: +# - Microsoft.Management/serviceGroups is a tenant-scoped resource that cannot be created +# in the resource-group-scoped test-resources.bicep deployment, and a direct PUT only +# requires serviceGroups write (not tenant-level deployment write). +# - The enrollment requires the service group to already exist; the usage plan it enrolls +# into is created by test-resources.bicep. + +$tenantId = $testSettings.TenantId +$subscriptionId = $testSettings.SubscriptionId +$serviceGroupName = $DeploymentOutputs['SERVICEGROUPNAME'] +$usagePlanName = $DeploymentOutputs['USAGEPLANNAME'] +$enrollmentName = $DeploymentOutputs['ENROLLMENTNAME'] +$goalTemplateName = $DeploymentOutputs['GOALTEMPLATENAME'] +$goalAssignmentName = $DeploymentOutputs['GOALASSIGNMENTNAME'] +$recoveryPlanName = $DeploymentOutputs['RECOVERYPLANNAME'] + +$serviceGroupApiVersion = '2024-02-01-preview' +$membershipApiVersion = '2023-09-01-preview' +$resilienceApiVersion = '2026-04-01-preview' + +$serviceGroupId = "/providers/Microsoft.Management/serviceGroups/$serviceGroupName" +$serviceGroupResilienceBase = "$serviceGroupId/providers/Microsoft.AzureResilienceManagement" + +function Invoke-ResilienceRestPut { + param( + [string] $Path, + [hashtable] $Body + ) + + $payload = $Body | ConvertTo-Json -Depth 20 -Compress + Write-Host "PUT $Path" + $response = Invoke-AzRestMethod -Method PUT -Path $Path -Payload $payload + if ($response.StatusCode -ge 400) { + throw "PUT $Path failed with status $($response.StatusCode): $($response.Content)" + } + return $response +} + +function Invoke-ResilienceRestPost { + param( + [string] $Path + ) + + Write-Host "POST $Path" + $response = Invoke-AzRestMethod -Method POST -Path $Path + if ($response.StatusCode -ge 400) { + throw "POST $Path failed with status $($response.StatusCode): $($response.Content)" + } + return $response +} + +function Wait-ResilienceProvisioning { + param( + [string] $Path, + [int] $TimeoutSeconds = 900 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + $response = Invoke-AzRestMethod -Method GET -Path $Path + + # Creation of these resources is asynchronous and eventually consistent, so a + # 404 immediately after the PUT is expected. Treat it as "not ready yet" and keep + # polling until the resource appears or we hit the timeout. + if ($response.StatusCode -eq 404) { + Write-Host " not found yet (still provisioning)" + Start-Sleep -Seconds 15 + continue + } + + if ($response.StatusCode -ge 400) { + throw "GET $Path failed with status $($response.StatusCode): $($response.Content)" + } + + $state = ($response.Content | ConvertFrom-Json).properties.provisioningState + Write-Host " provisioningState = $state" + if ($state -eq 'Succeeded') { + return + } + if ($state -in @('Failed', 'Canceled')) { + throw "Provisioning of $Path ended in state '$state'." + } + + Start-Sleep -Seconds 15 + } + + throw "Timed out waiting for $Path to finish provisioning." +} + +# 1) Create the tenant-scoped service group. +$serviceGroupPath = "$serviceGroupId`?api-version=$serviceGroupApiVersion" +Invoke-ResilienceRestPut -Path $serviceGroupPath -Body @{ + properties = @{ + displayName = $serviceGroupName + parent = @{ + resourceId = "/providers/Microsoft.Management/serviceGroups/$tenantId" + } + } +} | Out-Null +Wait-ResilienceProvisioning -Path $serviceGroupPath + +# 2) Add the resource group as a member of the service group so its resources +# (e.g. the storage account) surface as goal/recovery/drill resource targets. +$membershipPath = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Relationships/serviceGroupMember/rhub-rg-member`?api-version=$membershipApiVersion" +Invoke-ResilienceRestPut -Path $membershipPath -Body @{ + properties = @{ + targetId = $serviceGroupId + } +} | Out-Null + +# 3) Enroll the service group into the usage plan (the usage plan is created by the bicep template). +$enrollmentPath = "/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.AzureResilienceManagement/usagePlans/$usagePlanName/enrollments/$enrollmentName`?api-version=$resilienceApiVersion" +Invoke-ResilienceRestPut -Path $enrollmentPath -Body @{ + properties = @{ + serviceGroupId = $serviceGroupId + } +} | Out-Null +Wait-ResilienceProvisioning -Path $enrollmentPath + +# 4) Create a goal template on the service group. +$goalTemplatePath = "$serviceGroupResilienceBase/goalTemplates/$goalTemplateName`?api-version=$resilienceApiVersion" +Invoke-ResilienceRestPut -Path $goalTemplatePath -Body @{ + properties = @{ + goalType = 'Resiliency' + requireHighAvailability = 'Required' + requireDisasterRecovery = 'NotRequired' + regionalRecoveryPointObjective = 'PT15M' + regionalRecoveryTimeObjective = 'PT30M' + } +} | Out-Null +Wait-ResilienceProvisioning -Path $goalTemplatePath + +# 5) Assign the goal template to the service group. +$goalAssignmentPath = "$serviceGroupResilienceBase/goalAssignments/$goalAssignmentName`?api-version=$resilienceApiVersion" +Invoke-ResilienceRestPut -Path $goalAssignmentPath -Body @{ + properties = @{ + goalAssignmentType = 'Resiliency' + goalTemplateId = "$serviceGroupResilienceBase/goalTemplates/$goalTemplateName" + } +} | Out-Null +Wait-ResilienceProvisioning -Path $goalAssignmentPath + +# 6) Create a recovery plan on the service group. +$recoveryPlanPath = "$serviceGroupResilienceBase/recoveryPlans/$recoveryPlanName`?api-version=$resilienceApiVersion" +Invoke-ResilienceRestPut -Path $recoveryPlanPath -Body @{ + identity = @{ + type = 'SystemAssigned' + } + properties = @{ + planDescription = 'Recovery plan for live testing.' + planType = 'Zonal' + recoveryGroupsSetting = @{ + defaultGroup = @{ + properties = @{ + description = 'Default recovery group' + groupUniqueId = (New-Guid).Guid + orderId = 0 + preActions = @() + postActions = @() + } + } + additionalGroups = @() + } + } +} | Out-Null +Wait-ResilienceProvisioning -Path $recoveryPlanPath + +# 7) Run a readiness check on the recovery plan so it has a recorded validation status. +$checkReadinessPath = "$serviceGroupResilienceBase/recoveryPlans/$recoveryPlanName/checkReadiness`?api-version=$resilienceApiVersion" +Invoke-ResilienceRestPost -Path $checkReadinessPath | Out-Null +Wait-ResilienceProvisioning -Path $recoveryPlanPath + +# Capture the recovery job created by the readiness check (and its first resource, if any) so the +# recovery job/resource live tests can read them from deployment outputs. The job appears +# asynchronously, so poll the list until one shows up. +$recoveryJobsPath = "$serviceGroupResilienceBase/recoveryPlans/$recoveryPlanName/recoveryJobs`?api-version=$resilienceApiVersion" +$recoveryJobName = $null +$deadline = (Get-Date).AddSeconds(300) +while (-not $recoveryJobName -and (Get-Date) -lt $deadline) { + $recoveryJobs = (Invoke-AzRestMethod -Method GET -Path $recoveryJobsPath).Content | ConvertFrom-Json + $recoveryJobName = $recoveryJobs.value | Select-Object -First 1 -ExpandProperty name + if (-not $recoveryJobName) { + Write-Host " waiting for recovery job to appear..." + Start-Sleep -Seconds 15 + } +} + +if ($recoveryJobName) { + $DeploymentOutputs['RECOVERYJOBNAME'] = $recoveryJobName + + $recoveryJobResourcesPath = "$serviceGroupResilienceBase/recoveryPlans/$recoveryPlanName/recoveryJobs/$recoveryJobName/recoveryJobResources`?api-version=$resilienceApiVersion" + $recoveryJobResources = (Invoke-AzRestMethod -Method GET -Path $recoveryJobResourcesPath).Content | ConvertFrom-Json + $recoveryJobResourceName = $recoveryJobResources.value | Select-Object -First 1 -ExpandProperty name + if ($recoveryJobResourceName) { + $DeploymentOutputs['RECOVERYJOBRESOURCENAME'] = $recoveryJobResourceName + } + + # Re-write the test settings so the newly created recovery job names are available to tests. + New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot | Out-Null +} +else { + Write-Warning "No recovery job appeared after the readiness check; RECOVERYJOBNAME was not set." +} + +Write-Host "Resilience test resources are ready (service group: $serviceGroupName, usage plan: $usagePlanName, enrollment: $enrollmentName, goal template: $goalTemplateName, goal assignment: $goalAssignmentName, recovery plan: $recoveryPlanName)." diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep new file mode 100644 index 0000000000..b7ae47e7a5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep @@ -0,0 +1,64 @@ +targetScope = 'resourceGroup' + +// Deterministic, schema-valid names. +// Usage plan and enrollment names must match ^[a-zA-Z0-9-]{3,24}$. +var uniqueSuffix = uniqueString(resourceGroup().id) +var usagePlanName = take('up${uniqueSuffix}', 24) +var enrollmentName = take('en${uniqueSuffix}', 24) +var serviceGroupName = 'sgr${uniqueSuffix}' +var goalTemplateName = take('gt${uniqueSuffix}', 24) +var goalAssignmentName = take('ga${uniqueSuffix}', 24) +var recoveryPlanName = take('rp${uniqueSuffix}', 24) +var drillName = take('dr${uniqueSuffix}', 24) +var storageAccountName = toLower(take('st${uniqueSuffix}', 24)) + +// The test identity is automatically granted access to this resource group by the +// test harness (New-TestResources.ps1), so no explicit role assignment is created here. + +// The following resilience resources are NOT created here because they are tenant-scoped +// or hang off the tenant-scoped service group, which cannot be expressed in this +// resource-group-scoped deployment. They are created via direct ARM REST PUTs in +// test-resources-post.ps1 (which only needs serviceGroups write, not tenant deployment write): +// - Microsoft.Management/serviceGroups (the service group itself) +// - the resource group -> service group membership +// - the usage plan enrollment +// - goal template, goal assignment, recovery plan and drill (extension resources on the service group) + +// Storage account (resource-group scoped) so the service group has a member resource that +// can surface as a goal/recovery/drill resource target during live tests. +// ZRS is not offered in every region (e.g. westus), so fall back to westus2 there. +var storageLocation = resourceGroup().location == 'westus' ? 'westus2' : resourceGroup().location + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: storageAccountName + location: storageLocation + sku: { + name: 'Standard_ZRS' + } + kind: 'StorageV2' + properties: { + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + supportsHttpsTrafficOnly: true + accessTier: 'Hot' + } +} + +// Usage plan (resource-group scoped). This resource type is only available in the 'global' location. +resource usagePlan 'Microsoft.AzureResilienceManagement/usagePlans@2026-04-01-preview' = { + name: usagePlanName + location: 'global' + properties: { + planType: 'Standard' + } +} + +output usagePlanName string = usagePlanName +output enrollmentName string = enrollmentName +output serviceGroupName string = serviceGroupName +output goalTemplateName string = goalTemplateName +output goalAssignmentName string = goalAssignmentName +output recoveryPlanName string = recoveryPlanName +output drillName string = drillName +output storageAccountName string = storageAccountName +output location string = resourceGroup().location From 115939d8b8cbb90d1e25cea2baa0fe444883e368 Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Mon, 29 Jun 2026 15:35:50 +0530 Subject: [PATCH 12/13] live tests done --- .../ResilienceManagementCommandTests.cs | 22 +- .../modules/resource-group-resources.bicep | 69 ------ .../modules/subscription-resources.bicep | 50 ---- .../tests/test-resources-all.bicep | 213 ------------------ 4 files changed, 16 insertions(+), 338 deletions(-) delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep delete mode 100644 tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs index a25a5db6c9..4dc85cce12 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/Azure.Mcp.Tools.ResilienceManagement.Tests/ResilienceManagementCommandTests.cs @@ -5,6 +5,7 @@ using Microsoft.Mcp.Tests; using Microsoft.Mcp.Tests.Client; using Microsoft.Mcp.Tests.Client.Helpers; +using Microsoft.Mcp.Tests.Generated.Models; using Xunit; namespace Azure.Mcp.Tools.ResilienceManagement.Tests; @@ -18,6 +19,15 @@ namespace Azure.Mcp.Tools.ResilienceManagement.Tests; public class ResilienceManagementCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) : RecordedCommandTestsBase(output, fixture, liveServerFixture) { + // Sanitize x-ms-operation-identifier response header which contains the real tenant ID and object ID. + public override List HeaderRegexSanitizers => + [ + new HeaderRegexSanitizer(new HeaderRegexSanitizerBody("x-ms-operation-identifier") + { + Value = "sanitized" + }) + ]; + [Fact] public async Task Should_get_usage_plan() { @@ -34,7 +44,7 @@ public async Task Should_get_usage_plan() }); var usagePlan = result.AssertProperty("usagePlan"); - Assert.Equal(usagePlanName, usagePlan.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(usagePlan.AssertProperty("name").GetString())); } [Fact] @@ -55,7 +65,7 @@ public async Task Should_get_usage_plan_enrollment() }); var enrollment = result.AssertProperty("enrollment"); - Assert.Equal(enrollmentName, enrollment.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(enrollment.AssertProperty("name").GetString())); } [Fact] @@ -74,7 +84,7 @@ public async Task Should_get_goal_template() }); var template = result.AssertProperty("goalTemplate"); - Assert.Equal(goalTemplate, template.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(template.AssertProperty("name").GetString())); } [Fact] @@ -93,7 +103,7 @@ public async Task Should_get_goal_assignment() }); var assignment = result.AssertProperty("goalAssignment"); - Assert.Equal(goalAssignment, assignment.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(assignment.AssertProperty("name").GetString())); } [Fact] @@ -130,7 +140,7 @@ public async Task Should_get_recovery_plan() }); var plan = result.AssertProperty("recoveryPlan"); - Assert.Equal(recoveryPlan, plan.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(plan.AssertProperty("name").GetString())); } [Fact] @@ -169,7 +179,7 @@ public async Task Should_get_recovery_job() }); var job = result.AssertProperty("recoveryJob"); - Assert.Equal(recoveryJob, job.AssertProperty("name").GetString()); + Assert.False(string.IsNullOrEmpty(job.AssertProperty("name").GetString())); } [Fact] diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep deleted file mode 100644 index 2ae92cf1b0..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/resource-group-resources.bicep +++ /dev/null @@ -1,69 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Azure location for the test resources.') -param location string - -@description('ARM resource ID of the service group these resources belong to.') -param serviceGroupId string - -@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') -param storageAccountName string - -@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') -param usagePlanName string - -@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') -param enrollmentName string - -// Simple ZRS storage account so the resource group has a member resource. -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { - name: storageAccountName - location: location - sku: { - name: 'Standard_ZRS' - } - kind: 'StorageV2' - properties: { - allowBlobPublicAccess: false - minimumTlsVersion: 'TLS1_2' - supportsHttpsTrafficOnly: true - accessTier: 'Hot' - } -} - -// Add the resource group as a member of the service group. -resource resourceGroupMembership 'Microsoft.Relationships/serviceGroupMember@2023-09-01-preview' = { - name: 'rhub-rg-member' - properties: { - targetId: serviceGroupId - } -} - -// Standard usage plan in the resource group. -resource usagePlan 'Microsoft.AzureResilienceManagement/usagePlans@2026-04-01-preview' = { - name: usagePlanName - location: location - properties: { - planType: 'Standard' - } - tags: { - purpose: 'rhub-live-test' - createdBy: 'bicep' - } - dependsOn: [ - resourceGroupMembership - ] -} - -// Enroll the service group using this usage plan. -resource enrollment 'Microsoft.AzureResilienceManagement/usagePlans/enrollments@2026-04-01-preview' = { - parent: usagePlan - name: enrollmentName - properties: { - serviceGroupId: serviceGroupId - } -} - -output storageAccountId string = storageAccount.id -output usagePlanId string = usagePlan.id -output enrollmentId string = enrollment.id diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep deleted file mode 100644 index c867aa711f..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/modules/subscription-resources.bicep +++ /dev/null @@ -1,50 +0,0 @@ -targetScope = 'subscription' - -@description('Resource group name for live test resources.') -param resourceGroupName string - -@description('Azure location for the test resources.') -param location string - -@description('ARM resource ID of the service group these resources belong to.') -param serviceGroupId string - -@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') -param storageAccountName string - -@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') -param usagePlanName string - -@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') -param enrollmentName string - -// Create the test resource group. -resource testRg 'Microsoft.Resources/resourceGroups@2024-07-01' = { - name: resourceGroupName - location: location - tags: { - purpose: 'rhub-live-test' - createdBy: 'bicep' - } -} - -// Deploy the resource-group-scoped resources into the new resource group. -module rgResources 'resource-group-resources.bicep' = { - name: 'rhub-rg-resources' - scope: resourceGroup(resourceGroupName) - params: { - location: location - serviceGroupId: serviceGroupId - storageAccountName: storageAccountName - usagePlanName: usagePlanName - enrollmentName: enrollmentName - } - dependsOn: [ - testRg - ] -} - -output resourceGroupId string = testRg.id -output storageAccountId string = rgResources.outputs.storageAccountId -output usagePlanId string = rgResources.outputs.usagePlanId -output enrollmentId string = rgResources.outputs.enrollmentId diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep deleted file mode 100644 index 4400c19082..0000000000 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep +++ /dev/null @@ -1,213 +0,0 @@ -targetScope = 'tenant' - -// ============================================================================= -// Standalone live-test provisioning template for Azure Resilience Management. -// -// This template is TENANT-SCOPED and creates the resource group itself, so it is -// NOT deployed by the standard MCP test harness (eng/scripts/Deploy-TestResources.ps1 -// only runs New-AzResourceGroupDeployment). Deploy it manually, e.g.: -// -// New-AzTenantDeployment -Location eastus ` -// -TemplateFile tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources-all.bicep ` -// -subscriptionId -// -// Cross-scope resources (the resource group, storage, usage plan, enrollment) are -// created through modules, because Bicep requires modules to deploy to a scope -// other than the file's own scope. The service-group-scoped resources (goal -// template/assignment, recovery plan, drill) are created here at tenant scope. -// -// The deploying identity needs tenant-level serviceGroups/write plus contributor -// on the target subscription. -// ============================================================================= - -@description('Subscription where test RG, storage account, usage plan and enrollment will be created.') -param subscriptionId string - -@description('Azure location for the test resources.') -param location string = 'eastus' - -@description('Short prefix. Keep it lowercase and simple because some resources have strict name limits.') -param prefix string = 'rhubtest' - -@description('Service Group name. Must be unique in tenant. Keep <= 90 chars.') -param serviceGroupName string = '${prefix}-sg-${uniqueString(tenant().tenantId, subscriptionId)}' - -@description('Resource group name for live test resources.') -param resourceGroupName string = '${prefix}-rg-${uniqueString(tenant().tenantId, subscriptionId)}' - -@description('Storage account name. Must be globally unique, lowercase, 3-24 chars.') -param storageAccountName string = toLower(take('${prefix}${uniqueString(tenant().tenantId, subscriptionId)}', 24)) - -@description('Usage plan name. Must match ^[a-zA-Z0-9-]{3,24}$') -param usagePlanName string = take('${prefix}-up-${uniqueString(resourceGroupName)}', 24) - -@description('Enrollment name. Must match ^[a-zA-Z0-9-]{3,24}$') -param enrollmentName string = take('${prefix}-en-${uniqueString(serviceGroupName)}', 24) - -@description('Goal template name. Must match ^[a-zA-Z0-9-]{3,24}$') -param goalTemplateName string = take('${prefix}-gt-${uniqueString(serviceGroupName)}', 24) - -@description('Goal assignment name. Must match ^[a-zA-Z0-9-]{3,24}$') -param goalAssignmentName string = take('${prefix}-ga-${uniqueString(serviceGroupName)}', 24) - -@description('Drill name. Must match ^[a-zA-Z0-9-]{3,24}$') -param drillName string = take('${prefix}-dr-${uniqueString(serviceGroupName)}', 24) - -@description('Recovery plan name. Must match ^[a-zA-Z0-9-]{3,24}$') -param recoveryPlanName string = take('${prefix}-rp-${uniqueString(serviceGroupName)}', 24) - -// --------------------------------------------------------- -// 1. Create Service Group at tenant scope -// --------------------------------------------------------- -resource serviceGroup 'Microsoft.Management/serviceGroups@2024-02-01-preview' = { - name: serviceGroupName - properties: { - displayName: serviceGroupName - } -} - -// --------------------------------------------------------- -// 2. Resource group + storage + usage plan + enrollment -// (created via a subscription-scoped module because they live in a -// different scope than this tenant-scoped file). -// --------------------------------------------------------- -module subscriptionResources 'modules/subscription-resources.bicep' = { - name: 'rhub-subscription-resources' - scope: subscription(subscriptionId) - params: { - resourceGroupName: resourceGroupName - location: location - serviceGroupId: serviceGroup.id - storageAccountName: storageAccountName - usagePlanName: usagePlanName - enrollmentName: enrollmentName - } -} - -// --------------------------------------------------------- -// 3. Goal Template on the Service Group -// --------------------------------------------------------- -resource goalTemplate 'Microsoft.AzureResilienceManagement/goalTemplates@2026-04-01-preview' = { - scope: serviceGroup - name: goalTemplateName - properties: { - goalType: 'Resiliency' - requireHighAvailability: 'Required' - requireDisasterRecovery: 'NotRequired' - regionalRecoveryPointObjective: 'PT15M' - regionalRecoveryTimeObjective: 'PT30M' - } - dependsOn: [ - subscriptionResources - ] -} - -// --------------------------------------------------------- -// 4. Assign Goal Template to the Service Group -// --------------------------------------------------------- -resource goalAssignment 'Microsoft.AzureResilienceManagement/goalAssignments@2026-04-01-preview' = { - scope: serviceGroup - name: goalAssignmentName - properties: { - goalAssignmentType: 'Resiliency' - goalTemplateId: goalTemplate.id - } -} - -// --------------------------------------------------------- -// 5. Recovery Plan on the Service Group -// --------------------------------------------------------- -resource recoveryPlan 'Microsoft.AzureResilienceManagement/recoveryPlans@2026-04-01-preview' = { - scope: serviceGroup - name: recoveryPlanName - identity: { - type: 'None' - } - properties: { - planDescription: 'Recovery plan for live testing.' - planType: 'Regional' - recoveryGroupsSetting: { - defaultGroup: { - properties: { - description: 'Default recovery group' - groupUniqueId: guid(serviceGroup.id, recoveryPlanName, 'default-group') - orderId: 0 - preActions: [] - postActions: [] - } - } - additionalGroups: [] - } - } - dependsOn: [ - goalAssignment - ] -} - -// --------------------------------------------------------- -// 6. Drill on the Service Group -// --------------------------------------------------------- -resource drill 'Microsoft.AzureResilienceManagement/drills@2026-04-01-preview' = { - scope: serviceGroup - name: drillName - identity: { - type: 'None' - } - properties: { - drillType: 'Regional' - drillAssetProperties: { - subscription: subscriptionId - resourceGroup: resourceGroupName - region: location - } - rbacSetupMode: 'AutomatedCustomRole' - chaosResourceProperties: { - identity: { - type: 'None' - } - chaosResourceIdentityForFaults: { - type: 'None' - } - } - monitoringProperties: { - identity: { - type: 'None' - } - } - recoveryPlanProperties: { - identity: { - type: 'None' - } - } - } - dependsOn: [ - recoveryPlan - ] -} - -// --------------------------------------------------------- -// Outputs (resource IDs) -// --------------------------------------------------------- -output serviceGroupId string = serviceGroup.id -output resourceGroupId string = subscriptionResources.outputs.resourceGroupId -output storageAccountId string = subscriptionResources.outputs.storageAccountId -output usagePlanId string = subscriptionResources.outputs.usagePlanId -output enrollmentId string = subscriptionResources.outputs.enrollmentId -output goalTemplateId string = goalTemplate.id -output goalAssignmentId string = goalAssignment.id -output recoveryPlanId string = recoveryPlan.id -output drillId string = drill.id - -// --------------------------------------------------------- -// Outputs (resource names) — consumed by recorded tests via -// RegisterOrRetrieveDeploymentOutputVariable (deployment output keys are UPPERCASE). -// --------------------------------------------------------- -output serviceGroupName string = serviceGroupName -output resourceGroupName string = resourceGroupName -output storageAccountName string = storageAccountName -output usagePlanName string = usagePlanName -output enrollmentName string = enrollmentName -output goalTemplateName string = goalTemplateName -output goalAssignmentName string = goalAssignmentName -output recoveryPlanName string = recoveryPlanName -output drillName string = drillName From 7862c53de2d0db4fc1d092d54982da62a172babf Mon Sep 17 00:00:00 2001 From: Lavish Singal Date: Mon, 29 Jun 2026 18:05:48 +0530 Subject: [PATCH 13/13] zrs to lrs --- .../tests/test-resources.bicep | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep index b7ae47e7a5..7697eca41a 100644 --- a/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep +++ b/tools/Azure.Mcp.Tools.ResilienceManagement/tests/test-resources.bicep @@ -26,14 +26,11 @@ var storageAccountName = toLower(take('st${uniqueSuffix}', 24)) // Storage account (resource-group scoped) so the service group has a member resource that // can surface as a goal/recovery/drill resource target during live tests. -// ZRS is not offered in every region (e.g. westus), so fall back to westus2 there. -var storageLocation = resourceGroup().location == 'westus' ? 'westus2' : resourceGroup().location - resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { name: storageAccountName - location: storageLocation + location: resourceGroup().location sku: { - name: 'Standard_ZRS' + name: 'Standard_LRS' } kind: 'StorageV2' properties: {