File: ResourceExecutionConfigurationGathererTests.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 ASPIRECERTIFICATES001
 
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Tests;
 
public class ResourceExecutionConfigurationGathererTests
{
    #region ArgumentsExecutionConfigurationGatherer Tests
 
    [Fact]
    public async Task ArgumentsExecutionConfigurationGatherer_WithCommandLineArgsCallback_GathersArguments()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddExecutable("test", "test.exe", ".")
            .WithArgs("arg1", "arg2")
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new ArgumentsExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Equal(2, context.Arguments.Count);
        Assert.Equal("arg1", context.Arguments[0]);
        Assert.Equal("arg2", context.Arguments[1]);
    }
 
    [Fact]
    public async Task ArgumentsExecutionConfigurationGatherer_WithMultipleCallbacks_GathersAllArguments()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddExecutable("test", "test.exe", ".")
            .WithArgs("arg1")
            .WithArgs(ctx => ctx.Args.Add("arg2"))
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new ArgumentsExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Equal(2, context.Arguments.Count);
        Assert.Equal("arg1", context.Arguments[0]);
        Assert.Equal("arg2", context.Arguments[1]);
    }
 
    [Fact]
    public async Task ArgumentsExecutionConfigurationGatherer_NoArgsAnnotations_DoesNothing()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddExecutable("test", "test.exe", ".").Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new ArgumentsExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Empty(context.Arguments);
    }
 
    [Fact]
    public async Task ArgumentsExecutionConfigurationGatherer_AsyncCallback_ExecutesCorrectly()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddExecutable("test", "test.exe", ".")
            .WithArgs(async ctx =>
            {
                await Task.Delay(1);
                ctx.Args.Add("async-arg");
            })
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new ArgumentsExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Single(context.Arguments);
        Assert.Equal("async-arg", context.Arguments[0]);
    }
 
    #endregion
 
    #region EnvironmentVariablesExecutionConfigurationGatherer Tests
 
    [Fact]
    public async Task EnvironmentVariablesExecutionConfigurationGatherer_WithEnvironmentCallback_GathersEnvironmentVariables()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image")
            .WithEnvironment("KEY1", "value1")
            .WithEnvironment("KEY2", "value2")
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new EnvironmentVariablesExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Equal(2, context.EnvironmentVariables.Count);
        Assert.Equal("value1", context.EnvironmentVariables["KEY1"]);
        Assert.Equal("value2", context.EnvironmentVariables["KEY2"]);
    }
 
    [Fact]
    public async Task EnvironmentVariablesExecutionConfigurationGatherer_WithMultipleCallbacks_GathersAllVariables()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image")
            .WithEnvironment("KEY1", "value1")
            .WithEnvironment(ctx => ctx.EnvironmentVariables["KEY2"] = "value2")
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new EnvironmentVariablesExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Equal(2, context.EnvironmentVariables.Count);
        Assert.Equal("value1", context.EnvironmentVariables["KEY1"]);
        Assert.Equal("value2", context.EnvironmentVariables["KEY2"]);
    }
 
    [Fact]
    public async Task EnvironmentVariablesExecutionConfigurationGatherer_NoEnvironmentAnnotations_DoesNothing()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image").Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new EnvironmentVariablesExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Empty(context.EnvironmentVariables);
    }
 
    [Fact]
    public async Task EnvironmentVariablesExecutionConfigurationGatherer_AsyncCallback_ExecutesCorrectly()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image")
            .WithEnvironment(async ctx =>
            {
                await Task.Delay(1);
                ctx.EnvironmentVariables["ASYNC_KEY"] = "async-value";
            })
            .Resource;
 
        await builder.BuildAsync();
 
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new EnvironmentVariablesExecutionConfigurationGatherer();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        // Assert
        Assert.Single(context.EnvironmentVariables);
        Assert.Equal("async-value", context.EnvironmentVariables["ASYNC_KEY"]);
    }
 
    #endregion
 
    #region CertificateTrustExecutionConfigurationGatherer Tests
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_WithCertificateAuthorityCollection_SetsEnvironmentVariables()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var caCollection = builder.AddCertificateAuthorityCollection("test-ca").WithCertificate(cert);
 
        var resource = builder.AddContainer("test", "image")
            .WithCertificateAuthorityCollection(caCollection)
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Contains("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
        var metadata = context.AdditionalConfigurationData.OfType<CertificateTrustExecutionConfigurationData>().Single();
        Assert.Equal(CertificateTrustScope.Append, metadata.Scope);
        Assert.NotEmpty(metadata.Certificates);
    }
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_WithSystemScope_IncludesSystemCertificates()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var caCollection = builder.AddCertificateAuthorityCollection("test-ca").WithCertificate(cert);
 
        var resource = builder.AddContainer("test", "image")
            .WithCertificateAuthorityCollection(caCollection)
            .WithCertificateTrustScope(CertificateTrustScope.System)
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Contains("SSL_CERT_FILE", context.EnvironmentVariables.Keys);
        Assert.Contains("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
        var metadata = context.AdditionalConfigurationData.OfType<CertificateTrustExecutionConfigurationData>().Single();
        Assert.Equal(CertificateTrustScope.System, metadata.Scope);
        // System scope should include system root certificates
        Assert.True(metadata.Certificates.Count > 1);
    }
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_WithOverrideScope_SetsCorrectEnvironmentVariables()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var caCollection = builder.AddCertificateAuthorityCollection("test-ca").WithCertificate(cert);
 
        var resource = builder.AddContainer("test", "image")
            .WithCertificateAuthorityCollection(caCollection)
            .WithCertificateTrustScope(CertificateTrustScope.Override)
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Contains("SSL_CERT_FILE", context.EnvironmentVariables.Keys);
        Assert.Contains("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
        var metadata = context.AdditionalConfigurationData.OfType<CertificateTrustExecutionConfigurationData>().Single();
        Assert.Equal(CertificateTrustScope.Override, metadata.Scope);
    }
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_WithNoneScope_DoesNotSetEnvironmentVariables()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var caCollection = builder.AddCertificateAuthorityCollection("test-ca").WithCertificate(cert);
 
        var resource = builder.AddContainer("test", "image")
            .WithCertificateAuthorityCollection(caCollection)
            .WithCertificateTrustScope(CertificateTrustScope.None)
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.DoesNotContain("SSL_CERT_FILE", context.EnvironmentVariables.Keys);
        Assert.DoesNotContain("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
        var metadata = context.AdditionalConfigurationData.OfType<CertificateTrustExecutionConfigurationData>().Single();
        Assert.Equal(CertificateTrustScope.None, metadata.Scope);
    }
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_NoCertificateAnnotation_DoesNothing()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image").Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.DoesNotContain("SSL_CERT_FILE", context.EnvironmentVariables.Keys);
        Assert.DoesNotContain("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
    }
 
    [Fact]
    public async Task CertificateTrustExecutionConfigurationGatherer_WithAppendScope_DoesNotSetSSL_CERT_FILE()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var caCollection = builder.AddCertificateAuthorityCollection("test-ca").WithCertificate(cert);
 
        var resource = builder.AddContainer("test", "image")
            .WithCertificateAuthorityCollection(caCollection)
            .WithCertificateTrustScope(CertificateTrustScope.Append)
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateCertificateTrustConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new CertificateTrustExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.DoesNotContain("SSL_CERT_FILE", context.EnvironmentVariables.Keys);
        Assert.Contains("SSL_CERT_DIR", context.EnvironmentVariables.Keys);
    }
 
    #endregion
 
    #region HttpsCertificateExecutionConfigurationGatherer Tests
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_WithCertificate_ConfiguresMetadata()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
 
        var resource = builder.AddContainer("test", "image")
            .WithAnnotation(new HttpsCertificateAnnotation { Certificate = cert })
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        // Assert
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
        Assert.Equal(cert, metadata.Certificate);
        Assert.NotNull(metadata.KeyPathReference);
        Assert.NotNull(metadata.PfxPathReference);
    }
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_WithPassword_StoresPassword()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:password"] = "test-password";
        var cert = CreateTestCertificate();
        var password = builder.AddParameter("password", secret: true);
 
        var resource = builder.AddContainer("test", "image")
            .WithAnnotation(new HttpsCertificateAnnotation
            {
                Certificate = cert,
                Password = password.Resource
            })
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
        Assert.NotNull(metadata.Password);
    }
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_WithUseDeveloperCertificate_UsesDeveloperCert()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
 
        // Configure developer certificate service
        var devCert = CreateTestCertificate();
        builder.Services.AddSingleton<IDeveloperCertificateService>(new TestDeveloperCertificateService(devCert));
 
        var resource = builder.AddContainer("test", "image")
            .WithAnnotation(new HttpsCertificateAnnotation
            {
                UseDeveloperCertificate = true
            })
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
        Assert.Equal(devCert, metadata.Certificate);
    }
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_NoCertificateAnnotation_DoesNothing()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddContainer("test", "image").Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        Assert.Empty(context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>());
    }
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_TracksReferenceUsage()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
 
        var resource = builder.AddContainer("test", "image")
            .WithAnnotation(new HttpsCertificateAnnotation { Certificate = cert })
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var context = new ResourceExecutionConfigurationGathererContext();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
        // Assert
        var metadata = context.AdditionalConfigurationData.OfType<HttpsCertificateExecutionConfigurationData>().Single();
 
        // Initially, references should not be resolved
        Assert.False(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsPfxPathReferenced);
 
        // Accessing the references should mark them as resolved
        _ = await metadata.KeyPathReference.GetValueAsync(CancellationToken.None);
        Assert.True(metadata.IsKeyPathReferenced);
        Assert.False(metadata.IsPfxPathReferenced);
 
        _ = await metadata.PfxPathReference.GetValueAsync(CancellationToken.None);
        Assert.True(metadata.IsPfxPathReferenced);
    }
 
    [Fact]
    public async Task HttpsCertificateExecutionConfigurationGatherer_WithCallback_ExecutesCallback()
    {
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        var cert = CreateTestCertificate();
        var callbackExecuted = false;
 
        var resource = builder.AddContainer("test", "image")
            .WithAnnotation(new HttpsCertificateAnnotation { Certificate = cert })
            .WithAnnotation(new HttpsCertificateConfigurationCallbackAnnotation(ctx =>
            {
                callbackExecuted = true;
                return Task.CompletedTask;
            }))
            .Resource;
 
        await builder.BuildAsync();
 
        var configContextFactory = CreateHttpsCertificateConfigurationContextFactory();
        var gatherer = new HttpsCertificateExecutionConfigurationGatherer(configContextFactory);
        var context = new ResourceExecutionConfigurationGathererContext();
 
        // Act
        await gatherer.GatherAsync(context, resource, NullLogger.Instance, builder.ExecutionContext);
 
        // Assert
        Assert.True(callbackExecuted);
    }
 
    #endregion
 
    #region Helper Methods
 
    private static X509Certificate2 CreateTestCertificate()
    {
        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<CertificateTrustScope, CertificateTrustExecutionConfigurationContext> CreateCertificateTrustConfigurationContextFactory()
    {
        return scope => new CertificateTrustExecutionConfigurationContext
        {
            CertificateBundlePath = ReferenceExpression.Create($"/etc/ssl/certs/ca-bundle.crt"),
            CertificateDirectoriesPath = ReferenceExpression.Create($"/etc/ssl/certs")
        };
    }
 
    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"),
            PfxPath = ReferenceExpression.Create($"/etc/ssl/certs/server.pfx")
        };
    }
 
    private sealed class TestDeveloperCertificateService : IDeveloperCertificateService
    {
        private readonly X509Certificate2? _certificate;
 
        public TestDeveloperCertificateService(X509Certificate2? certificate = null)
        {
            _certificate = certificate;
        }
 
        public ImmutableList<X509Certificate2> Certificates =>
            _certificate != null ? [_certificate] : ImmutableList<X509Certificate2>.Empty;
 
        public bool SupportsContainerTrust => true;
 
        public bool TrustCertificate => true;
 
        public bool UseForHttps => true;
    }
 
    #endregion
}