Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<SuppressDependenciesWhenPacking>false</SuppressDependenciesWhenPacking>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.A365.DevTools.Cli.Tests" />
</ItemGroup>

<ItemGroup>
<!-- CLI Framework -->
<PackageReference Include="Microsoft.Identity.Client" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,16 +349,7 @@ public async Task<bool> 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);
Expand Down Expand Up @@ -1793,6 +1784,32 @@ private bool CheckNeedDeployment(JsonObject setupConfig)
return needDeployment;
}

/// <summary>
/// Ensures that an App Service Plan exists, creating it if necessary and verifying its existence.
/// </summary>
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.");
Comment on lines +1800 to +1807
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error details from the failed App Service plan creation are lost when throwing the exception. Consider capturing the result from AzWarnAsync or directly calling _executor.ExecuteAsync to access result.StandardError, which would provide more specific error information (e.g., quota limits vs. permissions) to include in the exception message.

Copilot uses AI. Check for mistakes.
}
_logger.LogInformation("App Service plan created successfully: {Plan}", planName);
}
}

/// <summary>
/// Get the Azure Web App runtime string based on the detected platform
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<A365SetupRunner> _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<ILogger<A365SetupRunner>>();
_commandExecutor = Substitute.For<CommandExecutor>(Substitute.For<ILogger<CommandExecutor>>());

// Create real instances with mocked logger for services that don't need complex mocking
var graphLogger = Substitute.For<ILogger<GraphApiService>>();
_graphService = new GraphApiService(graphLogger, _commandExecutor);

var webAppLogger = Substitute.For<ILogger<AzureWebAppCreator>>();
_webAppCreator = new AzureWebAppCreator(webAppLogger);

var delegatedLogger = Substitute.For<ILogger<DelegatedConsentService>>();
_delegatedConsentService = new DelegatedConsentService(delegatedLogger, _graphService);

var platformLogger = Substitute.For<ILogger<PlatformDetector>>();
_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<string>(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<string>(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<InvalidOperationException>(
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<string>(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<string>(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<string>(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<string>(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<string>(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<string>(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<string>(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<string>(s => s.Contains("appservice plan create") && s.Contains(planName)),
suppressErrorLogging: true)
.Returns(new CommandResult { ExitCode = 0, StandardOutput = "" });

// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
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<string>(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<string>(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<InvalidOperationException>(
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");
}
}
Loading