File: ProjectResourceBuilderExtensionTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// 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 ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental.
#pragma warning disable ASPIRECERTIFICATES001
#pragma warning disable ASPIREPROJECTS001 // WithProjectDefaults is experimental.
#pragma warning disable ASPIREEXTENSION001 // Debug support APIs are experimental.
 
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Tests;
 
[Trait("Partition", "2")]
public class ProjectResourceBuilderExtensionTests
{
    [Fact]
    public void ProjectLaunchDefaultsAnnotationIsTaggedWithExpectedExperimentalDiagnostic()
    {
        var attribute = Assert.Single(typeof(ProjectLaunchDefaultsAnnotation).GetCustomAttributes<ExperimentalAttribute>());
 
        Assert.Equal("ASPIREPROJECTS001", attribute.DiagnosticId);
        Assert.Equal("https://aka.ms/aspire/diagnostics/{0}", attribute.UrlFormat);
    }
 
    [Fact]
    public void WithProjectDefaultsIsTaggedWithExpectedExperimentalDiagnostic()
    {
        var method = typeof(ProjectResourceBuilderExtensions).GetMethod(
            nameof(ProjectResourceBuilderExtensions.WithProjectDefaults),
            BindingFlags.Public | BindingFlags.Static);
 
        Assert.NotNull(method);
        var attribute = Assert.Single(method!.GetCustomAttributes<ExperimentalAttribute>());
 
        Assert.Equal("ASPIREPROJECTS001", attribute.DiagnosticId);
        Assert.Equal("https://aka.ms/aspire/diagnostics/{0}", attribute.UrlFormat);
    }
 
    [Fact]
    public void WithPersistentLifetimeAddsPersistenceAnnotation()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var project = builder.AddProject<TestProject>("project", options => options.ExcludeLaunchProfile = true)
            .WithPersistentLifetime();
 
        var annotation = project.Resource.Annotations.OfType<PersistenceAnnotation>().Single();
        Assert.Equal(PersistenceMode.Persistent, annotation.Mode);
    }
 
    [Fact]
    public void WithProjectDefaultsThrowsClearExceptionWhenResourceHasNoProjectMetadata()
    {
        // WithProjectDefaults is public and only constrained to IResourceWithEnvironment /
        // IResourceWithEndpoints / IResourceWithArgs, so nothing stops a caller from invoking it on a
        // resource that never received an IProjectMetadata annotation. Without this guard, the first
        // internal .Single() over that annotation type throws a generic, unhelpful
        // "Sequence contains no elements" exception instead.
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var executable = builder.AddExecutable("exe", "dotnet", ".", "run");
 
        var exception = Assert.Throws<InvalidOperationException>(
            () => executable.WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true }));
 
        Assert.Contains(nameof(IProjectMetadata), exception.Message);
        Assert.Contains(executable.Resource.Name, exception.Message);
    }
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run)]
    [InlineData(DistributedApplicationOperation.Publish)]
    public void WithProjectDefaultsThrowsWhenAppliedTwice(DistributedApplicationOperation operation)
    {
        // Now that WithProjectDefaults is public, a caller can reach it again on a resource that
        // AddProject already applied it to. Almost all of the wiring is append-only: in run mode the
        // second pass tries to add a second "{name}-rebuilder" resource, which fails on the duplicate
        // name, and in both modes the debug support and environment callbacks would be duplicated.
        using var builder = TestDistributedApplicationBuilder.Create(operation);
 
        var project = builder.AddProject<TestProject>("project", options => options.ExcludeLaunchProfile = true);
 
        var exception = Assert.Throws<InvalidOperationException>(
            () => project.WithProjectDefaults(new ProjectResourceOptions { LaunchProfileName = "https" }));
 
        Assert.Contains("already been applied", exception.Message);
        Assert.Contains(project.Resource.Name, exception.Message);
    }
 
    [Fact]
    public void WithProjectDefaultsAppliesToAProjectResourceThatWasAddedDirectly()
    {
        // ProjectResource adds ProjectLaunchDefaultsAnnotation in its constructor, so the "already
        // applied" guard must key off the flag on the annotation rather than its presence.
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var project = builder.AddResource(new ProjectResource("project"))
                             .WithAnnotation<IProjectMetadata>(new TestProject())
                             .WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true });
 
        Assert.Single(project.Resource.Annotations.OfType<ProjectLaunchDefaultsAnnotation>());
    }
 
    [Fact]
    public void WithProjectDefaultsThrowsWhenResourceHasMultipleProjectMetadataAnnotations()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var project = builder.AddResource(new ProjectResource("project"))
                             .WithAnnotation<IProjectMetadata>(new TestProject())
                             .WithAnnotation<IProjectMetadata>(new OverrideTestProject());
 
        var exception = Assert.Throws<InvalidOperationException>(
            () => project.WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true }));
 
        Assert.Contains(project.Resource.Name, exception.Message);
        Assert.Contains("more than one", exception.Message);
    }
 
    [Theory]
    [InlineData(ResourceAnnotationMutationBehavior.Append, "more than one")]
    [InlineData(ResourceAnnotationMutationBehavior.Replace, "replaced")]
    public void ProjectMetadataConsumersRejectChangesAfterProjectDefaultsAreApplied(
        ResourceAnnotationMutationBehavior mutationBehavior,
        string expectedMessage)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<TestProject>("project", options => options.ExcludeLaunchProfile = true);
        project.WithAnnotation<IProjectMetadata>(new OverrideTestProject(), mutationBehavior);
 
        var exception = Assert.Throws<InvalidOperationException>(project.Resource.GetProjectMetadata);
 
        Assert.Contains(project.Resource.Name, exception.Message);
        Assert.Contains(expectedMessage, exception.Message);
    }
 
    [Fact]
    public async Task WithProjectDefaultsAddsHttpsCertificateConfigurationForTlsEndpoints()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:password"] = "test-password";
        var cert = CreateTestCertificateWithPrivateKey();
        var password = builder.AddParameter("password", secret: true);
 
        var resource = builder.AddProject<TestProject>("test", options => options.ExcludeLaunchProfile = true)
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert, password)
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        var certificatePath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath]);
        Assert.Equal("/etc/ssl/certs/server.pfx", await certificatePath.GetValueAsync());
        Assert.Same(password.Resource, context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword]);
 
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
        Assert.False(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsCertificateWithKeyPathReferenced);
        Assert.True(metadata.IsPfxPathReferenced);
    }
 
    [Fact]
    public async Task WithProjectDefaultsRemovesStaleKestrelCertificatePassword()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificateWithPrivateKey();
 
        var resource = builder.AddProject<TestProject>("test", options => options.ExcludeLaunchProfile = true)
            .WithHttpsEndpoint()
            .WithEnvironment(KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword, "stale-password")
            .WithHttpsCertificate(cert)
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new EnvironmentVariablesExecutionConfigurationGatherer()
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        var certificatePath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath]);
        Assert.Equal("/etc/ssl/certs/server.pfx", await certificatePath.GetValueAsync());
        Assert.DoesNotContain(KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword, context.EnvironmentVariables.Keys);
    }
 
    [Fact]
    public async Task WithProjectDefaultsDoesNotAddHttpsCertificateConfigurationWithoutTlsEndpoints()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificateWithPrivateKey();
 
        var resource = builder.AddProject<TestProject>("test", options => options.ExcludeLaunchProfile = true)
            .WithHttpsCertificate(cert)
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        Assert.Empty(context.EnvironmentVariables);
 
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
        Assert.False(metadata.IsPfxPathReferenced);
    }
 
    [Theory]
    [InlineData(true, false)]
    [InlineData(true, true)]
    [InlineData(false, false)]
    [InlineData(false, true)]
    public async Task WithProjectDefaultsPreservesPemCertificateConfiguration(bool configureBeforeDefaults, bool hasPassword)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:password"] = "test-password";
        using var cert = CreateTestCertificateWithPrivateKey();
        var password = hasPassword ? builder.AddParameter("password", secret: true) : null;
        var expectedEnvironment = new Dictionary<string, object>();
 
        var project = builder.AddResource(new ProjectResource("test"))
            .WithAnnotation<IProjectMetadata>(new TestProject())
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert, password);
 
        if (configureBeforeDefaults)
        {
            project.WithHttpsCertificateConfiguration(ConfigureCertificate);
        }
 
        project.WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true });
 
        if (!configureBeforeDefaults)
        {
            project.WithHttpsCertificateConfiguration(ConfigureCertificate);
        }
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, project.Resource, NullLogger.Instance, builder.ExecutionContext);
 
        AssertSameEnvironment(expectedEnvironment, context.EnvironmentVariables);
 
        var certificatePath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath]);
        var keyPath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultKeyPath]);
        Assert.Equal("/etc/ssl/certs/server.crt", await certificatePath.GetValueAsync());
        Assert.Equal("/etc/ssl/private/server.key", await keyPath.GetValueAsync());
 
        var metadata = Assert.Single(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
        Assert.True(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsCertificateWithKeyPathReferenced);
        Assert.False(metadata.IsPfxPathReferenced);
 
        Task ConfigureCertificate(HttpsCertificateConfigurationCallbackAnnotationContext callbackContext)
        {
            expectedEnvironment[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath] = callbackContext.CertificatePath;
            expectedEnvironment[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultKeyPath] = callbackContext.KeyPath;
            if (callbackContext.Password is not null)
            {
                expectedEnvironment[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword] = callbackContext.Password;
            }
 
            foreach (var (name, value) in expectedEnvironment)
            {
                callbackContext.EnvironmentVariables[name] = value;
            }
 
            return Task.CompletedTask;
        }
    }
 
    [Theory]
    [InlineData("pfx", true, false)]
    [InlineData("pfx", false, true)]
    [InlineData("pem", true, true)]
    [InlineData("pem", false, false)]
    [InlineData("store", true, false)]
    [InlineData("store", false, true)]
    public async Task WithProjectDefaultsPreservesCertificateEnvironment(string certificateFormat, bool configureBeforeDefaults, bool hasAspirePassword)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:password"] = "aspire-password";
        using var cert = CreateTestCertificateWithPrivateKey();
        var password = hasAspirePassword ? builder.AddParameter("password", secret: true) : null;
        Dictionary<string, string> environment = certificateFormat switch
        {
            "pfx" => new()
            {
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath] = "custom.pfx",
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword] = "custom-password"
            },
            "pem" => new()
            {
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath] = "custom.crt",
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultKeyPath] = "custom.key",
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword] = "custom-password"
            },
            "store" => new()
            {
                [KnownAspNetCoreConfigNames.KestrelCertificatesDefaultSubject] = "custom-certificate",
                ["Kestrel__Certificates__Default__Store"] = "My",
                ["Kestrel__Certificates__Default__Location"] = "CurrentUser"
            },
            _ => throw new ArgumentOutOfRangeException(nameof(certificateFormat))
        };
 
        var project = builder.AddResource(new ProjectResource("test"))
            .WithAnnotation<IProjectMetadata>(new TestProject())
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert, password);
 
        if (configureBeforeDefaults)
        {
            ConfigureEnvironment();
        }
 
        project.WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true });
 
        if (!configureBeforeDefaults)
        {
            ConfigureEnvironment();
        }
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new EnvironmentVariablesExecutionConfigurationGatherer()
            .GatherAsync(context, project.Resource, NullLogger.Instance, builder.ExecutionContext);
        var expectedEnvironment = new Dictionary<string, object>(context.EnvironmentVariables);
 
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, project.Resource, NullLogger.Instance, builder.ExecutionContext);
 
        AssertSameEnvironment(expectedEnvironment, context.EnvironmentVariables);
 
        var metadata = Assert.Single(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
        Assert.False(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsCertificateWithKeyPathReferenced);
        Assert.False(metadata.IsPfxPathReferenced);
 
        void ConfigureEnvironment()
        {
            foreach (var (name, value) in environment)
            {
                project.WithEnvironment(name, value);
            }
        }
    }
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath, "")]
    [InlineData(DistributedApplicationOperation.Publish, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath, "custom.pfx")]
    [InlineData(DistributedApplicationOperation.Run, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultKeyPath, "")]
    [InlineData(DistributedApplicationOperation.Publish, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultKeyPath, "custom.key")]
    [InlineData(DistributedApplicationOperation.Run, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultSubject, "")]
    [InlineData(DistributedApplicationOperation.Publish, KnownAspNetCoreConfigNames.KestrelCertificatesDefaultSubject, "custom-certificate")]
    [InlineData(DistributedApplicationOperation.Run, "KESTREL__CERTIFICATES__DEFAULT__PATH", "custom.pfx")]
    [InlineData(DistributedApplicationOperation.Publish, "kestrel__certificates__default__keypath", "custom.key")]
    [InlineData(DistributedApplicationOperation.Run, "kestrel__Certificates__default__Subject", "custom-certificate")]
    public async Task WithProjectDefaultsPreservesExplicitCertificateSelection(DistributedApplicationOperation operation, string name, string value)
    {
        using var builder = TestDistributedApplicationBuilder.Create(operation);
        using var cert = CreateTestCertificateWithPrivateKey();
        var resource = builder.AddProject<TestProject>("test", options => options.ExcludeLaunchProfile = true)
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert)
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        context.EnvironmentVariables[name] = value;
        var expectedEnvironment = new Dictionary<string, object>(context.EnvironmentVariables);
 
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        AssertSameEnvironment(expectedEnvironment, context.EnvironmentVariables);
 
        var metadata = Assert.Single(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
        Assert.False(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsCertificateWithKeyPathReferenced);
        Assert.False(metadata.IsPfxPathReferenced);
    }
 
    [Fact]
    public async Task WithProjectDefaultsDoesNotResolveExplicitCertificateSelection()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        using var cert = CreateTestCertificateWithPrivateKey();
        var expectedEnvironment = new Dictionary<string, object>();
        var resource = builder.AddResource(new ProjectResource("test"))
            .WithAnnotation<IProjectMetadata>(new TestProject())
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert)
            .WithHttpsCertificateConfiguration(context =>
            {
                // Wrapping a tracked reference detects both replacement and eager resolution.
                var path = ReferenceExpression.Create($"{context.PfxPath}");
                context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath] = path;
                expectedEnvironment[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath] = path;
                return Task.CompletedTask;
            })
            .WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true })
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        AssertSameEnvironment(expectedEnvironment, context.EnvironmentVariables);
 
        var metadata = Assert.Single(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
        Assert.False(metadata.IsPfxPathReferenced);
    }
 
    [Theory]
    [InlineData("OTHER_CERTIFICATE_PATH")]
    [InlineData("Kestrel__Certificates__Default__Store")]
    [InlineData("Kestrel__Certificates__Default__Location")]
    [InlineData("Kestrel__Certificates__Default__AllowInvalid")]
    [InlineData(KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword)]
    [InlineData("Kestrel__Certificates__Default__PathSuffix")]
    [InlineData("Kestrel__Certificates__Named__Path")]
    [InlineData("Kestrel__Endpoints__https__Certificate__Path")]
    public async Task WithProjectDefaultsAddsCertificateDefaultsWithoutExplicitSelection(string name)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        using var cert = CreateTestCertificateWithPrivateKey();
        var resource = builder.AddResource(new ProjectResource("test"))
            .WithAnnotation<IProjectMetadata>(new TestProject())
            .WithHttpsEndpoint()
            .WithHttpsCertificate(cert)
            .WithHttpsCertificateConfiguration(context =>
            {
                context.EnvironmentVariables[name] = "existing";
                return Task.CompletedTask;
            })
            .WithProjectDefaults(new ProjectResourceOptions { ExcludeLaunchProfile = true })
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        var certificatePath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath]);
        Assert.Equal("/etc/ssl/certs/server.pfx", await certificatePath.GetValueAsync());
        if (name == KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword)
        {
            Assert.Single(context.EnvironmentVariables);
        }
        else
        {
            Assert.Equal(2, context.EnvironmentVariables.Count);
            Assert.Equal("existing", context.EnvironmentVariables[name]);
        }
 
        var metadata = Assert.Single(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
        Assert.False(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsCertificateWithKeyPathReferenced);
        Assert.True(metadata.IsPfxPathReferenced);
    }
 
    [Fact]
    public async Task WithProjectDefaultsReplacesStaleKestrelCertificatePassword()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:password"] = "test-password";
        using var cert = CreateTestCertificateWithPrivateKey();
        var password = builder.AddParameter("password", secret: true);
        var resource = builder.AddProject<TestProject>("test", options => options.ExcludeLaunchProfile = true)
            .WithHttpsEndpoint()
            .WithEnvironment(KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword, "stale-password")
            .WithHttpsCertificate(cert, password)
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ExecutionConfigurationGathererContext();
        await new EnvironmentVariablesExecutionConfigurationGatherer()
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        await new HttpsCertificateExecutionConfigurationGatherer(CreateHttpsCertificateConfigurationContextFactory())
            .GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        var certificatePath = Assert.IsAssignableFrom<IValueProvider>(context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPath]);
        Assert.Equal("/etc/ssl/certs/server.pfx", await certificatePath.GetValueAsync());
        Assert.Same(password.Resource, context.EnvironmentVariables[KnownAspNetCoreConfigNames.KestrelCertificatesDefaultPassword]);
    }
 
    private static void AssertSameEnvironment(IReadOnlyDictionary<string, object> expected, IReadOnlyDictionary<string, object> actual)
    {
        Assert.Equal(expected.Count, actual.Count);
        foreach (var (name, value) in expected)
        {
            Assert.Same(value, actual[name]);
        }
    }
 
    private static X509Certificate2 CreateTestCertificateWithPrivateKey()
    {
        using var rsa = RSA.Create(2048);
        var request = new CertificateRequest(
            new X500DistinguishedName("CN=test"),
            rsa,
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
 
        return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));
    }
 
    private static Func<X509Certificate2, HttpsCertificateExecutionConfigurationContext> CreateHttpsCertificateConfigurationContextFactory()
    {
        return cert => new HttpsCertificateExecutionConfigurationContext
        {
            CertificatePath = ReferenceExpression.Create($"/etc/ssl/certs/server.crt"),
            KeyPath = ReferenceExpression.Create($"/etc/ssl/private/server.key"),
            CertificateWithKeyPath = ReferenceExpression.Create($"/etc/ssl/certs/server.pem"),
            PfxPath = ReferenceExpression.Create($"/etc/ssl/certs/server.pfx")
        };
    }
 
    private sealed class TestProject : IProjectMetadata
    {
        public string ProjectPath => "test.csproj";
    }
 
    private sealed class OverrideTestProject : IProjectMetadata
    {
        public string ProjectPath => "override.csproj";
    }
}