File: Commands\SecretCommandTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Commands;
using Aspire.Cli.Projects;
using Aspire.Cli.Secrets;
using Aspire.Cli.Tests.Utils;
using Aspire.Shared.UserSecrets;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
 
namespace Aspire.Cli.Tests.Commands;
 
public class SecretCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task SecretPathCommand_PrintsSecretsPath_ForDotNetAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        var expectedPath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Development");
 
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
 
        using var provider = CreateSecretTestServices(
            workspace,
            outputWriter,
            appHostFile,
            userSecretsId);
 
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse($"secret path --apphost \"{appHostFile.FullName}\"");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Contains(expectedPath, outputWriter.Logs);
    }
 
    [Fact]
    public async Task SecretPathCommand_PrintsSecretsPath_ForGuestAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"));
        var userSecretsId = UserSecretsPathHelper.ComputeSyntheticUserSecretsId(appHostFile.FullName);
        var expectedPath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Development");
 
        await File.WriteAllTextAsync(appHostFile.FullName, "export {};");
 
        using var provider = CreateSecretTestServices(
            workspace,
            outputWriter,
            appHostFile,
            userSecretsId);
 
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse($"secret path --apphost \"{appHostFile.FullName}\"");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Contains(expectedPath, outputWriter.Logs);
    }
 
    [Fact]
    public async Task SecretCommands_UseEnvironmentSpecificAspireSecretsFile()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        var developmentPath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Development");
        var productionPath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Production");
 
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
 
        using var provider = CreateSecretTestServices(
            workspace,
            outputWriter,
            appHostFile,
            userSecretsId);
 
        var command = provider.GetRequiredService<RootCommand>();
 
        var setResult = command.Parse($"secret set --environment Production Parameters:api_key prod-secret --apphost \"{appHostFile.FullName}\"");
        var setExitCode = await setResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, setExitCode);
        Assert.False(File.Exists(developmentPath));
        Assert.True(File.Exists(productionPath));
 
        outputWriter.Logs.Clear();
        var getResult = command.Parse($"secret get --environment Production Parameters:api_key --apphost \"{appHostFile.FullName}\"");
        var getExitCode = await getResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, getExitCode);
        Assert.Contains("prod-secret", outputWriter.Logs);
 
        outputWriter.Logs.Clear();
        var listResult = command.Parse($"secret list --environment Production --format json --apphost \"{appHostFile.FullName}\"");
        var listExitCode = await listResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, listExitCode);
        Assert.Contains(outputWriter.Logs, line => line.Contains("Parameters:api_key", StringComparison.Ordinal));
 
        var defaultGetResult = command.Parse($"secret get Parameters:api_key --apphost \"{appHostFile.FullName}\"");
        var defaultGetExitCode = await defaultGetResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.ConfigNotFound, defaultGetExitCode);
 
        var deleteResult = command.Parse($"secret delete --environment Production Parameters:api_key --apphost \"{appHostFile.FullName}\"");
        var deleteExitCode = await deleteResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, deleteExitCode);
 
        var deletedGetResult = command.Parse($"secret get --environment Production Parameters:api_key --apphost \"{appHostFile.FullName}\"");
        var deletedGetExitCode = await deletedGetResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.ConfigNotFound, deletedGetExitCode);
    }
 
    [Fact]
    public async Task SecretCommands_PreserveDeploymentSecretsAfterDevelopmentInitializesUserSecrets()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        var project = new TestAppHostProject(userSecretsId, initializeOnDemand: true);
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
        using var provider = CreateSecretTestServices(workspace, outputWriter, appHostFile, project);
        var command = provider.GetRequiredService<RootCommand>();
        var projectOption = $"--apphost \"{appHostFile.FullName}\"";
        var syntheticPath = AspireSecretsStoreResolver.GetSecretsFilePath(
            workspace.WorkspaceRoot, AspireSecretsPathHelper.ComputeSyntheticAppHostId(appHostFile.FullName), "Production");
 
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret set --environment Production Parameters:key prod-secret {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.False(project.IsInitialized);
        Assert.True(File.Exists(syntheticPath));
 
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret set Parameters:key dev-secret {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.True(project.IsInitialized);
        Assert.False(File.Exists(syntheticPath));
        Assert.False(File.Exists(UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId)));
 
        outputWriter.Logs.Clear();
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret get --environment Production Parameters:key {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains("prod-secret", outputWriter.Logs);
 
        outputWriter.Logs.Clear();
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret list --all --format json {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains(outputWriter.Logs, line => line.Contains("prod-secret", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("dev-secret", StringComparison.Ordinal));
 
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret delete --environment Production Parameters:key {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.ConfigNotFound, await command.Parse($"secret get --environment Production Parameters:key {projectOption}").InvokeAsync().DefaultTimeout());
    }
 
    [Fact]
    public async Task SecretListCommand_AllIncludesLowercaseDevelopmentEnvironment()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
        using var provider = CreateSecretTestServices(workspace, outputWriter, appHostFile, userSecretsId);
        var command = provider.GetRequiredService<RootCommand>();
        var projectOption = $"--apphost \"{appHostFile.FullName}\"";
 
        Assert.Equal(CliExitCodes.Success, await command.Parse(
            $"secret set --environment development Parameters:key lowercase-secret {projectOption}").InvokeAsync().DefaultTimeout());
        outputWriter.Logs.Clear();
 
        Assert.Equal(CliExitCodes.Success, await command.Parse(
            $"secret list --all --format json {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"development\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"lowercase-secret\"", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task SecretListCommand_CanListAllEnvironmentSecrets()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        var developmentUserSecretsPath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
        var developmentAspirePath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Development");
 
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
 
        using var provider = CreateSecretTestServices(
            workspace,
            outputWriter,
            appHostFile,
            userSecretsId);
 
        var command = provider.GetRequiredService<RootCommand>();
 
        var setDevelopmentResult = command.Parse($"secret set Parameters:api_key dev-secret --apphost \"{appHostFile.FullName}\"");
        var setDevelopmentExitCode = await setDevelopmentResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, setDevelopmentExitCode);
        Assert.False(File.Exists(developmentUserSecretsPath));
        Assert.True(File.Exists(developmentAspirePath));
 
        var setProductionResult = command.Parse($"secret set --environment Production Azure:SubscriptionId prod-subscription --apphost \"{appHostFile.FullName}\"");
        var setProductionExitCode = await setProductionResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, setProductionExitCode);
 
        outputWriter.Logs.Clear();
        var listAllResult = command.Parse($"secret list --all --format json --apphost \"{appHostFile.FullName}\"");
        var listAllExitCode = await listAllResult.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, listAllExitCode);
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"Development\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"Parameters:api_key\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"dev-secret\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"Production\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"Azure:SubscriptionId\"", StringComparison.Ordinal));
        Assert.Contains(outputWriter.Logs, line => line.Contains("\"prod-subscription\"", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task SecretSet_DefaultDevelopmentImportsLegacyUserSecretsAndWritesAspireSecrets()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        var userSecretsPath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
        var aspirePath = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Development");
 
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
        Directory.CreateDirectory(Path.GetDirectoryName(userSecretsPath)!);
        await File.WriteAllTextAsync(userSecretsPath, """
            {
              "Legacy:Value": "old"
            }
            """);
 
        try
        {
            using var provider = CreateSecretTestServices(
                workspace,
                outputWriter,
                appHostFile,
                userSecretsId);
 
            var command = provider.GetRequiredService<RootCommand>();
 
            var getLegacyResult = command.Parse($"secret get Legacy:Value --apphost \"{appHostFile.FullName}\"");
            var getLegacyExitCode = await getLegacyResult.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, getLegacyExitCode);
            Assert.Contains("old", outputWriter.Logs);
 
            var setResult = command.Parse($"secret set New:Value new --apphost \"{appHostFile.FullName}\"");
            var setExitCode = await setResult.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, setExitCode);
 
            var userSecretsStore = new SecretsStore(userSecretsPath);
            Assert.Equal("old", userSecretsStore.Get("Legacy:Value"));
            Assert.Null(userSecretsStore.Get("New:Value"));
 
            var aspireStore = new SecretsStore(aspirePath);
            Assert.Equal("old", aspireStore.Get("Legacy:Value"));
            Assert.Equal("new", aspireStore.Get("New:Value"));
        }
        finally
        {
            if (File.Exists(userSecretsPath))
            {
                File.Delete(userSecretsPath);
            }
        }
    }
 
    [Theory]
    [InlineData("set Parameters:key value")]
    [InlineData("get Parameters:key")]
    [InlineData("list")]
    [InlineData("delete Parameters:key")]
    [InlineData("path")]
    public void SecretCommands_RejectInvalidEnvironmentNamesBeforeExecution(string arguments)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        using var provider = CreateSecretTestServices(workspace, outputWriter, appHostFile, Guid.NewGuid().ToString("N"));
        var command = provider.GetRequiredService<RootCommand>();
 
        foreach (var environment in new[] { "prod/eu", "Production.", ".Production", "CON", "com1", new string('a', 81), "" })
        {
            var result = command.Parse($"secret {arguments} --environment \"{environment}\"");
            Assert.Contains(result.Errors, error => error.Message == Aspire.Cli.Resources.SecretCommandStrings.InvalidEnvironmentName);
        }
 
        Assert.False(Directory.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "secrets")));
    }
 
    [Theory]
    [InlineData("Production", "production")]
    [InlineData("production", "PRODUCTION")]
    [InlineData("prod-eu", "PROD-EU")]
    public async Task SecretCommands_CaseInsensitiveCrudPreservesEnvironmentSpelling(string originalEnvironment, string alternateEnvironment)
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
        using var provider = CreateSecretTestServices(workspace, outputWriter, appHostFile, userSecretsId);
        var command = provider.GetRequiredService<RootCommand>();
        var projectOption = $"--apphost \"{appHostFile.FullName}\"";
 
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret set Parameters:key initial -e {originalEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret set Parameters:key updated -e {alternateEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
 
        outputWriter.Logs.Clear();
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret get Parameters:key -e {alternateEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains("updated", outputWriter.Logs);
 
        outputWriter.Logs.Clear();
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret list --all --format json {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains(outputWriter.Logs, line => line.Contains($"\"{originalEnvironment}\"", StringComparison.Ordinal));
        var directory = AspireSecretsStoreResolver.GetSecretsDirectoryPath(workspace.WorkspaceRoot, userSecretsId);
        Assert.Equal($"{originalEnvironment}.json", Path.GetFileName(Assert.Single(Directory.EnumerateFiles(directory))));
 
        outputWriter.Logs.Clear();
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret path -e {alternateEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Contains(Path.Combine(directory, $"{originalEnvironment}.json"), outputWriter.Logs);
 
        Assert.Equal(CliExitCodes.Success, await command.Parse($"secret delete Parameters:key -e {alternateEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.ConfigNotFound, await command.Parse($"secret get Parameters:key -e {originalEnvironment} {projectOption}").InvokeAsync().DefaultTimeout());
    }
 
    [Theory]
    [InlineData("set Parameters:new new-value", "Parameters:new", "new-value")]
    [InlineData("set parameters:TARGET updated-value", "Parameters:Target", "updated-value")]
    [InlineData("delete parameters:TARGET", "Parameters:Target", null)]
    public async Task SecretEditsPreserveUnrelatedDeploymentStateTypes(string arguments, string key, string? expectedValue)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var outputWriter = new TestOutputTextWriter(outputHelper);
        var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj"));
        var userSecretsId = Guid.NewGuid().ToString("N");
        await File.WriteAllTextAsync(appHostFile.FullName, "<Project />");
        var path = AspireSecretsStoreResolver.GetSecretsFilePath(workspace.WorkspaceRoot, userSecretsId, "Production");
        SecretsStore.SaveJson(path, JsonNode.Parse("""
            {
              "Resource": {
                "Count": 42,
                "Enabled": true,
                "Ratio": 1.25,
                "Missing": null,
                "Text": "value"
              },
              "Parameters:Target": "original"
            }
            """)!.AsObject());
        using var provider = CreateSecretTestServices(workspace, outputWriter, appHostFile, userSecretsId);
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse($"secret {arguments} -e Production --apphost \"{appHostFile.FullName}\"")
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        var persisted = JsonNode.Parse(await File.ReadAllTextAsync(path))!.AsObject();
        Assert.Equal(42, persisted["Resource:Count"]!.GetValue<int>());
        Assert.True(persisted["Resource:Enabled"]!.GetValue<bool>());
        Assert.Equal(1.25, persisted["Resource:Ratio"]!.GetValue<double>());
        Assert.True(persisted.ContainsKey("Resource:Missing"));
        Assert.Null(persisted["Resource:Missing"]);
        Assert.Equal("value", persisted["Resource:Text"]!.GetValue<string>());
 
        var store = new SecretsStore(path);
        Assert.Equal(expectedValue, store.Get(key));
        Assert.Equal(expectedValue is not null, store.ContainsKey(key));
    }
 
    private ServiceProvider CreateSecretTestServices(
        TemporaryWorkspace workspace,
        TestOutputTextWriter outputWriter,
        FileInfo appHostFile,
        string userSecretsId)
        => CreateSecretTestServices(workspace, outputWriter, appHostFile, new TestAppHostProject(userSecretsId));
 
    private ServiceProvider CreateSecretTestServices(
        TemporaryWorkspace workspace,
        TestOutputTextWriter outputWriter,
        FileInfo appHostFile,
        IAppHostProject project)
    {
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(
                workspace.WorkspaceRoot, homeDirectory: workspace.WorkspaceRoot);
            options.OutputTextWriter = outputWriter;
            options.DisableAnsi = true;
            options.ProjectLocatorFactory = _ => new TestProjectLocator(appHostFile);
        });
 
        services.Replace(ServiceDescriptor.Singleton<IAppHostProjectFactory>(
            new TestAppHostProjectFactory(project)));
 
        return services.BuildServiceProvider();
    }
 
    private sealed class TestProjectLocator(FileInfo appHostFile) : IProjectLocator
    {
        public Task<List<AppHostProjectCandidate>> FindAppHostProjectsAsync(DirectoryInfo searchDirectory, AppHostDiscoveryScope scope, CancellationToken cancellationToken)
            => Task.FromResult<List<AppHostProjectCandidate>>([new(appHostFile, "test")]);
 
        public Task<List<FileInfo>> FindAppHostProjectFilesAsync(DirectoryInfo searchDirectory, AppHostDiscoveryScope scope, CancellationToken cancellationToken)
            => Task.FromResult<List<FileInfo>>([appHostFile]);
 
        public Task<FileInfo?> GetAppHostFromSettingsAsync(CancellationToken cancellationToken = default)
            => Task.FromResult<FileInfo?>(appHostFile);
 
        public Task<FileInfo?> UseOrFindAppHostProjectFileAsync(FileInfo? projectFile, bool createSettingsFile, CancellationToken cancellationToken)
            => Task.FromResult<FileInfo?>(projectFile ?? appHostFile);
 
        public Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(FileInfo? projectFile, MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, bool createSettingsFile, CancellationToken cancellationToken = default)
            => Task.FromResult(new AppHostProjectSearchResult(projectFile ?? appHostFile, [projectFile ?? appHostFile]));
    }
 
    private sealed class TestAppHostProjectFactory(IAppHostProject project) : IAppHostProjectFactory
    {
        public IAppHostProject GetProject(LanguageInfo language) => project;
 
        public IAppHostProject? TryGetProject(FileInfo appHostFile) => project;
 
        public IAppHostProject GetProject(FileInfo appHostFile) => project;
    }
 
    private sealed class TestAppHostProject(string userSecretsId, bool initializeOnDemand = false) : IAppHostProject
    {
        public bool IsInitialized { get; private set; } = !initializeOnDemand;
        public bool IsUnsupported { get; set; }
        public string LanguageId => "test";
        public string DisplayName => "Test";
        public bool SupportsLaunchProfiles => false;
        public string? AppHostFileName => null;
 
        public Task<bool> AddPackageAsync(AddPackageContext context, CancellationToken cancellationToken) => throw new NotSupportedException();
        public bool CanHandle(FileInfo appHostFile) => true;
        public Task<RunningInstanceResult> FindAndStopRunningInstanceAsync(FileInfo appHostFile, DirectoryInfo homeDirectory, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<string[]> GetDetectionPatternsAsync(CancellationToken cancellationToken = default) => Task.FromResult<string[]>([]);
        public Task<string?> GetUserSecretsIdAsync(FileInfo appHostFile, bool autoInit, CancellationToken cancellationToken)
        {
            IsInitialized |= autoInit;
            return Task.FromResult(IsInitialized ? userSecretsId : null);
        }
        public bool IsUsingProjectReferences(FileInfo appHostFile) => false;
        public Task<int> PublishAsync(PublishContext context, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<int> RunAsync(AppHostProjectContext context, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<UpdatePackagesResult> UpdatePackagesAsync(UpdatePackagesContext context, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<AppHostValidationResult> ValidateAppHostAsync(FileInfo appHostFile, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<string?> GetAspireHostingVersionAsync(FileInfo appHostFile, CancellationToken cancellationToken) => throw new NotSupportedException();
    }
}