| File: SingleFileAppHostInitDotnetRunTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.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 System.Text.Json.Nodes; using Aspire.Cli.EndToEnd.Tests.Helpers; using Hex1b.Automation; using Hex1b.Input; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// Regression test for https://github.com/microsoft/aspire/issues/15986. /// </summary> /// <remarks> /// <para> /// Verifies that after interactive <c>aspire init</c>'s C# single-file path runs, /// <c>dotnet run apphost.cs</c> can launch the AppHost successfully — i.e. the /// launch-profile environment variables required by the dashboard / OTLP / resource /// service are wired up correctly via the generated <c>apphost.run.json</c>. /// </para> /// <para> /// Before the fix, <c>aspire init</c> wrote <c>apphost.cs</c>, <c>aspire.config.json</c>, /// and <c>NuGet.config</c> but skipped <c>apphost.run.json</c>. The .NET 10 file-based /// runner only honours <c>[file].run.json</c> for launch profiles (it ignores /// <c>aspire.config.json</c>), so without it <c>dotnet run apphost.cs</c> crashed at /// startup with <c>OptionsValidationException</c> complaining that /// <c>ASPNETCORE_URLS</c> and <c>ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL</c> were not set. /// </para> /// </remarks> public sealed class SingleFileAppHostInitDotnetRunTests(ITestOutputHelper output) { [CaptureWorkspaceOnFailure] [Fact] public async Task AspireInitSingleFileAppHostRunsViaDotnetRunAppHost() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Run aspire init without --language so the interactive language prompt is shown, // then accept the default '> C#' selection. await auto.TypeAsync("aspire init"); await auto.EnterAsync(); await auto.WaitUntilAsync( s => new CellPatternSearcher().Find("> C#").Search(s).Count > 0, timeout: TimeSpan.FromSeconds(30), description: "language selection prompt with default '> C#'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("Created aspire.config.json", timeout: TimeSpan.FromMinutes(2)); await auto.DeclineAgentInitPromptAsync(counter); // The workspace directory is bind-mounted into the container, so we can read // the files generated by `aspire init` from the host side without leaving the // shell session. var appHostCs = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"); var aspireConfigJson = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire.config.json"); var appHostRunJson = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.run.json"); Assert.True(File.Exists(appHostCs), $"Expected apphost.cs to exist at: {appHostCs}"); Assert.True(File.Exists(aspireConfigJson), $"Expected aspire.config.json to exist at: {aspireConfigJson}"); Assert.Contains("#:property AspireUseCliBundle=true", File.ReadAllText(appHostCs)); Assert.True( File.Exists(appHostRunJson), $"Expected apphost.run.json to exist at: {appHostRunJson}. " + "This is the regression tracked by https://github.com/microsoft/aspire/issues/15986: " + "without it, `dotnet run apphost.cs` crashes at startup because the dashboard " + "environment variables (ASPNETCORE_URLS, ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL) are unset."); // Sanity-check that the launch profile in apphost.run.json carries the dashboard // env vars that the file-based runner needs to inject. Full schema is covered by // InitCommand_SingleFileSkeleton_CreatesAppHostRunJsonWithDashboardEnvVars. var runJson = JsonNode.Parse(File.ReadAllText(appHostRunJson))?.AsObject(); Assert.NotNull(runJson); var httpsProfile = runJson["profiles"]?["https"]?.AsObject(); Assert.NotNull(httpsProfile); Assert.Equal("Project", httpsProfile["commandName"]?.GetValue<string>()); Assert.False(string.IsNullOrWhiteSpace(httpsProfile["applicationUrl"]?.GetValue<string>())); var httpsEnv = httpsProfile["environmentVariables"]?.AsObject(); Assert.NotNull(httpsEnv); Assert.False(string.IsNullOrWhiteSpace(httpsEnv["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]?.GetValue<string>())); Assert.False(string.IsNullOrWhiteSpace(httpsEnv["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]?.GetValue<string>())); // The generated AppHost opts into the CLI bundle, so `dotnet run` delegates to the // CLI and displays its startup message instead of the direct AppHost log message. await auto.TypeAsync("dotnet run apphost.cs"); await auto.EnterAsync(); await auto.WaitUntilTextAsync( "Press CTRL+C to stop the AppHost and exit.", timeout: TimeSpan.FromMinutes(1)); // Stop the running AppHost with Ctrl+C and wait for the shell prompt. await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromMinutes(1)); } }