File: RadiusAzureResourcesDeploymentTests.cs
Web Access
Project: src\tests\Aspire.Deployment.EndToEnd.Tests\Aspire.Deployment.EndToEnd.Tests.csproj (Aspire.Deployment.EndToEnd.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Deployment.EndToEnd.Tests.Helpers;
using Hex1b.Automation;
using Xunit;
 
namespace Aspire.Deployment.EndToEnd.Tests;
 
/// <summary>
/// Gap-documenting tests for cloud-managed Azure resource references when publishing to Radius.
/// </summary>
/// <remarks>
/// Radius 0.59 supports portable recipe-backed resources such as <c>AddRedis</c>, but the
/// Aspire Radius publisher does not currently translate cloud-managed Azure resources such as
/// Key Vault, Storage, Service Bus, or Azure Managed Redis into Radius resources. Today
/// <c>aspire publish</c> hangs in that scenario; the tracking issue
/// (https://github.com/microsoft/aspire/issues/18802) covers making it fail fast instead. The
/// test is marked <c>[ActiveIssue]</c> and asserts that intended fail-fast behavior, so it starts
/// passing once the gap is closed.
/// </remarks>
public sealed class RadiusAzureResourcesDeploymentTests(ITestOutputHelper output)
{
    [Fact]
    [ActiveIssue("https://github.com/microsoft/aspire/issues/18802")]
    public async Task PublishWithAzureKeyVaultReferenceDocumentsCurrentRadiusGap()
    {
        if (!DeploymentE2ETestHelpers.IsRunningInCI)
        {
            var localCurrentBuildStrategy = DeploymentE2ETestHelpers.GetCurrentBuildCliInstallStrategy();
            if (localCurrentBuildStrategy.Mode is CliInstallMode.InstallScript &&
                localCurrentBuildStrategy.Quality is null &&
                localCurrentBuildStrategy.Version is null)
            {
                Assert.Skip("Local Radius/Azure gap testing requires an explicit current-build CLI strategy. Set ASPIRE_E2E_ARCHIVE to a localhive archive or configure ASPIRE_E2E_QUALITY, ASPIRE_E2E_VERSION, or GITHUB_PR_NUMBER.");
            }
        }
 
        using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(8));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            cts.Token, TestContext.Current.CancellationToken);
        var cancellationToken = linkedCts.Token;
 
        using var workspace = TemporaryWorkspace.Create(output);
        const string projectName = "RadiusAzureGap";
 
        using var terminal = DeploymentE2ETestHelpers.CreateTestTerminal();
        var pendingRun = terminal.RunAsync(cancellationToken);
 
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
 
        await auto.PrepareEnvironmentAsync(workspace, counter);
        await auto.InstallCurrentBuildAspireCliAsync(counter, output);
 
        output.WriteLine("Creating an empty AppHost for the Radius/Azure resource gap check...");
        await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost);
 
        await auto.TypeAsync($"cd {projectName}");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        await auto.TypeAsync("aspire add Aspire.Hosting.Radius --all --non-interactive");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(3));
 
        await auto.TypeAsync("aspire add Aspire.Hosting.Azure.KeyVault --all --non-interactive");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(3));
 
        var appHostFilePath = Path.Combine(
            workspace.WorkspaceRoot.FullName,
            projectName,
            $"{projectName}.AppHost",
            "AppHost.cs");
 
        File.WriteAllText(appHostFilePath,
            """
            // Licensed to the .NET Foundation under one or more agreements.
            // The .NET Foundation licenses this file to you under the MIT license.
 
            #pragma warning disable ASPIREPIPELINES001
            #pragma warning disable ASPIRERADIUS003
 
            var builder = DistributedApplication.CreateBuilder(args);
 
            var radius = builder.AddRadiusEnvironment("radius")
                .WithAzureProvider(
                    subscriptionId: "11111111-1111-1111-1111-111111111111",
                    resourceGroup: "rg-radius-gap",
                    azure => azure.WithWorkloadIdentity(
                        tenantId: "22222222-2222-2222-2222-222222222222",
                        clientId: "33333333-3333-3333-3333-333333333333"));
 
            var vault = builder.AddAzureKeyVault("vault");
 
            builder.AddContainer("consumer", "mcr.microsoft.com/azuredocs/aci-helloworld", "latest")
                .WithImageSHA256("456a1150aa41340a14c7be1342deda2cde9e6e7df9fde6b8a69de0ae04f92fad")
                .WithReference(vault)
                .WithComputeEnvironment(radius);
 
            builder.Build().Run();
            """);
 
        await auto.TypeAsync($"cd {projectName}.AppHost");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        output.WriteLine("Publishing should fail fast once the gap is fixed: Aspire cannot translate a cloud-managed Azure resource (Key Vault) into a Radius resource. See https://github.com/microsoft/aspire/issues/18802.");
        await auto.TypeAsync(
            "set +e; " +
            "rm -rf ../out ../publish.log; " +
            // The bug in #18802 causes `aspire publish` to HANG (Radius resolves Azure Bicep outputs
            // during container env emission). Cap it with `timeout` so an unfixed build fails the
            // test loudly instead of hanging. Once #18802 is fixed, publish should fail fast with a
            // non-zero exit and produce no app.bicep.
            "timeout 120s aspire publish --output-path ../out --non-interactive > ../publish.log 2>&1; " +
            "code=$?; " +
            "if [ \"$code\" = \"124\" ]; then cat ../publish.log; echo \"Radius publish still hangs on a cloud-managed Azure reference (see issue 18802).\"; exit 1; fi; " +
            "if [ \"$code\" = \"0\" ]; then cat ../publish.log; echo \"Radius publish unexpectedly succeeded for a cloud-managed Azure reference; it should fail fast until Azure resource translation is supported.\"; exit 1; fi; " +
            "if [ -f ../out/app.bicep ]; then cat ../publish.log; echo \"Radius publish produced app.bicep for a cloud-managed Azure reference; it should fail before emitting output.\"; exit 1; fi; " +
            // Split the marker token in the source command (RADIUS_AZURE_REFERENCE_GAP''_FAILFAST
            // evaluates to RADIUS_AZURE_REFERENCE_GAP_FAILFAST) so Hex1b cannot self-match the
            // echoed command line before the shell actually reaches the gap assertion.
            "echo RADIUS_AZURE_REFERENCE_GAP''_FAILFAST; " +
            "exit 0");
        await auto.EnterAsync();
        await auto.WaitUntilAsync(
            s => new CellPatternSearcher().Find("RADIUS_AZURE_REFERENCE_GAP_FAILFAST").Search(s).Count > 0,
            timeout: TimeSpan.FromMinutes(3),
            description: "Radius/Azure reference gap fail-fast marker");
        await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30));
 
        await auto.TypeAsync("exit");
        await auto.EnterAsync();
 
        await pendingRun;
    }
}