From 7694c591333a9f7154fb68cd65cd6652b23fcc31 Mon Sep 17 00:00:00 2001 From: Lavanya Kappagantu Date: Mon, 24 Nov 2025 11:12:33 -0800 Subject: [PATCH] Throwing exception when service plan is not created --- .../Microsoft.Agents.A365.DevTools.Cli.csproj | 4 + .../Services/A365SetupRunner.cs | 37 ++- .../Services/A365SetupRunnerTests.cs | 220 ++++++++++++++++++ 3 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/A365SetupRunnerTests.cs diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Microsoft.Agents.A365.DevTools.Cli.csproj b/src/Microsoft.Agents.A365.DevTools.Cli/Microsoft.Agents.A365.DevTools.Cli.csproj index a17b2743..77220e5a 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Microsoft.Agents.A365.DevTools.Cli.csproj +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Microsoft.Agents.A365.DevTools.Cli.csproj @@ -16,6 +16,10 @@ false + + + + diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index e195fe6c..984f4afd 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs @@ -349,16 +349,7 @@ public async Task RunAsync(string configPath, string generatedConfigPath, } // App Service plan - var planShow = await _executor.ExecuteAsync("az", $"appservice plan show -g {resourceGroup} -n {planName} --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true); - if (planShow.Success) - { - _logger.LogInformation("App Service plan already exists: {Plan} (skipping creation)", planName); - } - else - { - _logger.LogInformation("Creating App Service plan {Plan}", planName); - await AzWarnAsync($"appservice plan create -g {resourceGroup} -n {planName} --sku {planSku} --is-linux --subscription {subscriptionId}", "Create App Service plan"); - } + await EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId); // Web App var webShow = await _executor.ExecuteAsync("az", $"webapp show -g {resourceGroup} -n {webAppName} --subscription {subscriptionId}", captureOutput: true); @@ -1793,6 +1784,32 @@ private bool CheckNeedDeployment(JsonObject setupConfig) return needDeployment; } + /// + /// Ensures that an App Service Plan exists, creating it if necessary and verifying its existence. + /// + internal async Task EnsureAppServicePlanExistsAsync(string resourceGroup, string planName, string planSku, string subscriptionId) + { + var planShow = await _executor.ExecuteAsync("az", $"appservice plan show -g {resourceGroup} -n {planName} --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true); + if (planShow.Success) + { + _logger.LogInformation("App Service plan already exists: {Plan} (skipping creation)", planName); + } + else + { + _logger.LogInformation("Creating App Service plan {Plan}", planName); + await AzWarnAsync($"appservice plan create -g {resourceGroup} -n {planName} --sku {planSku} --is-linux --subscription {subscriptionId}", "Create App Service plan"); + + // Verify the plan was created successfully + var verifyPlan = await _executor.ExecuteAsync("az", $"appservice plan show -g {resourceGroup} -n {planName} --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true); + if (!verifyPlan.Success) + { + _logger.LogError("ERROR: App Service plan creation failed. The plan '{Plan}' does not exist.", planName); + throw new InvalidOperationException($"Failed to create App Service plan '{planName}'. This may be due to quota limits or insufficient permissions. Setup cannot continue."); + } + _logger.LogInformation("App Service plan created successfully: {Plan}", planName); + } + } + /// /// Get the Azure Web App runtime string based on the detected platform /// diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/A365SetupRunnerTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/A365SetupRunnerTests.cs new file mode 100644 index 00000000..344e8459 --- /dev/null +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/A365SetupRunnerTests.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentAssertions; +using Microsoft.Agents.A365.DevTools.Cli.Services; +using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Microsoft.Agents.A365.DevTools.Cli.Tests.Services; + +public class A365SetupRunnerTests +{ + private readonly ILogger _logger; + private readonly CommandExecutor _commandExecutor; + private readonly GraphApiService _graphService; + private readonly AzureWebAppCreator _webAppCreator; + private readonly DelegatedConsentService _delegatedConsentService; + private readonly PlatformDetector _platformDetector; + private readonly A365SetupRunner _setupRunner; + + public A365SetupRunnerTests() + { + _logger = Substitute.For>(); + _commandExecutor = Substitute.For(Substitute.For>()); + + // Create real instances with mocked logger for services that don't need complex mocking + var graphLogger = Substitute.For>(); + _graphService = new GraphApiService(graphLogger, _commandExecutor); + + var webAppLogger = Substitute.For>(); + _webAppCreator = new AzureWebAppCreator(webAppLogger); + + var delegatedLogger = Substitute.For>(); + _delegatedConsentService = new DelegatedConsentService(delegatedLogger, _graphService); + + var platformLogger = Substitute.For>(); + _platformDetector = new PlatformDetector(platformLogger); + + _setupRunner = new A365SetupRunner( + _logger, + _commandExecutor, + _graphService, + _webAppCreator, + _delegatedConsentService, + _platformDetector); + } + + [Fact] + public async Task EnsureAppServicePlanExists_WhenQuotaLimitExceeded_ThrowsInvalidOperationException() + { + // Arrange + var subscriptionId = "test-sub-id"; + var resourceGroup = "test-rg"; + var planName = "test-plan"; + var planSku = "B1"; + + // Mock app service plan doesn't exist (initial check) + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true) + .Returns(new CommandResult { ExitCode = 1, StandardError = "Plan not found" }); + + // Mock app service plan creation fails with quota error + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create") && s.Contains(planName)), + suppressErrorLogging: true) + .Returns(new CommandResult + { + ExitCode = 1, + StandardError = "ERROR: Operation cannot be completed without additional quota.\n\nAdditional details - Location:\n\nCurrent Limit (Basic VMs): 0\n\nCurrent Usage: 0\n\nAmount required for this deployment (Basic VMs): 1" + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await _setupRunner.EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId)); + + exception.Message.Should().Contain($"Failed to create App Service plan '{planName}'"); + exception.Message.Should().Contain("quota limits"); + } + + [Fact] + public async Task EnsureAppServicePlanExists_WhenPlanAlreadyExists_SkipsCreation() + { + // Arrange + var subscriptionId = "test-sub-id"; + var resourceGroup = "test-rg"; + var planName = "existing-plan"; + var planSku = "B1"; + + // Mock app service plan already exists + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true) + .Returns(new CommandResult + { + ExitCode = 0, + StandardOutput = "{\"name\": \"existing-plan\", \"sku\": {\"name\": \"B1\"}}" + }); + + // Act + await _setupRunner.EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId); + + // Assert - Verify creation command was never called + await _commandExecutor.DidNotReceive().ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create")), + suppressErrorLogging: true); + } + + [Fact] + public async Task EnsureAppServicePlanExists_WhenCreationSucceeds_VerifiesExistence() + { + // Arrange + var subscriptionId = "test-sub-id"; + var resourceGroup = "test-rg"; + var planName = "new-plan"; + var planSku = "B1"; + + // Mock app service plan doesn't exist initially, then exists after creation + var planShowCallCount = 0; + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true) + .Returns(callInfo => + { + planShowCallCount++; + // First call: plan doesn't exist, second call (after creation): plan exists + return planShowCallCount == 1 + ? new CommandResult { ExitCode = 1, StandardError = "Plan not found" } + : new CommandResult { ExitCode = 0, StandardOutput = "{\"name\": \"new-plan\"}" }; + }); + + // Mock app service plan creation succeeds + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create") && s.Contains(planName)), + suppressErrorLogging: true) + .Returns(new CommandResult { ExitCode = 0, StandardOutput = "Plan created" }); + + // Act + await _setupRunner.EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId); + + // Assert - Verify the plan creation was called + await _commandExecutor.Received(1).ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create") && s.Contains(planName)), + suppressErrorLogging: true); + + // Verify the plan was checked twice (before creation and verification after) + await _commandExecutor.Received(2).ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true); + } + + [Fact] + public async Task EnsureAppServicePlanExists_WhenCreationFailsSilently_ThrowsInvalidOperationException() + { + // Arrange - Tests the scenario where Azure CLI returns success but the plan doesn't actually exist + var subscriptionId = "test-sub-id"; + var resourceGroup = "test-rg"; + var planName = "failed-plan"; + var planSku = "B1"; + + // Mock app service plan doesn't exist before and after creation attempt + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true) + .Returns(new CommandResult { ExitCode = 1, StandardError = "Plan not found" }); + + // Mock plan creation appears to succeed but doesn't actually create the plan + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create") && s.Contains(planName)), + suppressErrorLogging: true) + .Returns(new CommandResult { ExitCode = 0, StandardOutput = "" }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await _setupRunner.EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId)); + + exception.Message.Should().Contain($"Failed to create App Service plan '{planName}'"); + } + + [Fact] + public async Task EnsureAppServicePlanExists_WhenPermissionDenied_ThrowsInvalidOperationException() + { + // Arrange + var subscriptionId = "test-sub-id"; + var resourceGroup = "test-rg"; + var planName = "test-plan"; + var planSku = "B1"; + + // Mock app service plan doesn't exist + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan show") && s.Contains(planName)), + captureOutput: true, + suppressErrorLogging: true) + .Returns(new CommandResult { ExitCode = 1, StandardError = "Plan not found" }); + + // Mock app service plan creation fails with permission error + _commandExecutor.ExecuteAsync("az", + Arg.Is(s => s.Contains("appservice plan create") && s.Contains(planName)), + suppressErrorLogging: true) + .Returns(new CommandResult + { + ExitCode = 1, + StandardError = "ERROR: The client does not have authorization to perform action" + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await _setupRunner.EnsureAppServicePlanExistsAsync(resourceGroup, planName, planSku, subscriptionId)); + + exception.Message.Should().Contain($"Failed to create App Service plan '{planName}'"); + exception.Message.Should().Contain("insufficient permissions"); + } +}