File: AppServiceDotnetProjectDeploymentTests.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;
 
public sealed class AppServiceDotnetProjectDeploymentTests(ITestOutputHelper output)
{
    [Fact]
    public async Task DeployDotnetProjectWithStaticFrontendToAzureAppService()
    {
        var subscriptionId = DotnetProjectDeploymentHelpers.GetSubscriptionId();
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
        cts.CancelAfter(TimeSpan.FromMinutes(45));
        using var workspace = TemporaryWorkspace.Create(output);
        var resourceGroupName = DeploymentE2ETestHelpers.GenerateResourceGroupName("appservice-v2");
        var startTime = DateTime.UtcNow;
        const string projectName = "DotnetAppService";
        var marker = $"appservice-{Guid.NewGuid():N}";
 
        try
        {
            using var terminal = DeploymentE2ETestHelpers.CreateTestTerminal();
            var pendingRun = terminal.RunAsync(cts.Token);
            var counter = new SequenceCounter();
            var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
            await auto.PrepareEnvironmentAsync(workspace, counter);
            await auto.InstallCurrentBuildAspireCliAsync(counter, output);
            await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.JsReact, useRedisCache: false);
            await auto.RunCommandAsync($"cd {projectName}", counter);
            await DotnetProjectDeploymentHelpers.AddPackageAsync(auto, counter, "Aspire.Hosting.Azure.AppService");
            await DotnetProjectDeploymentHelpers.AddPackageAsync(auto, counter, "Aspire.Hosting.Dotnet");
 
            var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, projectName);
            var appHostFile = Path.Combine(projectDir, $"{projectName}.AppHost", "AppHost.cs");
            var content = DotnetProjectDeploymentHelpers.ReplaceExactlyOnce(File.ReadAllText(appHostFile),
                $"AddProject<Projects.{projectName}_Server>(\"server\")",
                $"AddDotnetProject(\"server\", \"../{projectName}.Server/{projectName}.Server.csproj\")");
            Assert.Contains("server.PublishWithContainerFiles(webfrontend, \"wwwroot\");", content, StringComparison.Ordinal);
            content = DotnetProjectDeploymentHelpers.ReplaceExactlyOnce(content, "builder.Build().Run();",
                "builder.AddAzureAppServiceEnvironment(\"infra\");\nbuilder.Build().Run();");
            File.WriteAllText(appHostFile, "#pragma warning disable ASPIREDOTNETPROJECT001\n" + content);
            DotnetProjectDeploymentHelpers.ReplaceInFile(Path.Combine(projectDir, $"{projectName}.Server", "Program.cs"),
                "app.Run();", $$"""
                app.MapGet("/api/deployment-marker", () => "{{marker}}");
                app.Run();
                """);
            // Vite copies public files to its build output; this must reach the .NET image through
            // PublishWithContainerFiles, not a separate frontend site or an empty deployment slot.
            var publicDir = Directory.CreateDirectory(Path.Combine(projectDir, "frontend", "public"));
            File.WriteAllText(Path.Combine(publicDir.FullName, "deployment-marker.txt"), $"static:{marker}");
 
            await auto.RunCommandAsync($"cd {projectName}.AppHost", counter);
            await auto.RunCommandAsync(
                $"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 AZURE__RESOURCEGROUP={resourceGroupName} AZURE__SUBSCRIPTIONID={subscriptionId}", counter);
            await auto.TypeAsync("aspire deploy --clear-cache");
            await auto.EnterAsync();
            await auto.WaitForPipelineSuccessAsync(TimeSpan.FromMinutes(30), counter: counter);
            await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));
 
            var urlFile = Path.Combine(projectDir, "server-url.txt");
            await DotnetProjectDeploymentHelpers.RunScriptAsync(auto, counter, $$"""
                set -euo pipefail
                az() { command az "$@" --subscription {{subscriptionId}}; }
                site=$(az webapp list -g {{resourceGroupName}} --query "[?starts_with(name, 'server')].name" -o tsv)
                [ -n "$site" ] && [ "$(printf '%s\n' "$site" | wc -l)" -eq 1 ]
                [ "$(az webapp deployment slot list -g {{resourceGroupName}} -n "$site" --query 'length(@)' -o tsv)" -eq 0 ]
                [ "$(az webapp config show -g {{resourceGroupName}} -n "$site" --query linuxFxVersion -o tsv)" = SITECONTAINERS ]
                id=$(az webapp show -g {{resourceGroupName}} -n "$site" --query id -o tsv)
                image=$(az rest --method get --url "https://management.azure.com$id/sitecontainers/main?api-version=2024-11-01" --query properties.image -o tsv)
                case "$image" in *.azurecr.io/*) ;; *) echo "Unexpected server image: $image"; exit 1;; esac
                echo "$site: $image"
                az webapp show -g {{resourceGroupName}} -n "$site" --query defaultHostName -o tsv > {{AspireCliShellCommandHelpers.QuoteBashArg(urlFile)}}
                """, TimeSpan.FromMinutes(3));
            var url = $"https://{File.ReadAllText(urlFile).Trim()}";
            await Task.WhenAll(
                DotnetProjectDeploymentHelpers.VerifyResponseAsync($"{url}/api/deployment-marker", marker, cts.Token),
                DotnetProjectDeploymentHelpers.VerifyResponseAsync($"{url}/deployment-marker.txt", $"static:{marker}", cts.Token));
            await auto.TypeAsync("exit");
            await auto.EnterAsync();
            await pendingRun;
            DeploymentReporter.ReportDeploymentSuccess(nameof(DeployDotnetProjectWithStaticFrontendToAzureAppService),
                resourceGroupName, new Dictionary<string, string> { ["server"] = url }, DateTime.UtcNow - startTime);
        }
        catch (Exception ex)
        {
            DeploymentReporter.ReportDeploymentFailure(nameof(DeployDotnetProjectWithStaticFrontendToAzureAppService),
                resourceGroupName, ex.Message, ex.StackTrace);
            throw;
        }
        finally
        {
            await DotnetProjectDeploymentHelpers.CleanupAsync(resourceGroupName, subscriptionId, output);
        }
    }
}