File: ConditionalReferenceExpressionTests.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.
 
using System.Net.Sockets;
using System.Text.Json;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Tests;
 
public class ConditionalReferenceExpressionTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task GetValueAsync_ReturnsTrueValue_WhenConditionIsTrue()
    {
        var condition = new TestValueProvider(bool.TrueString);
        var whenTrue = ReferenceExpression.Create($",ssl=true");
        var whenFalse = ReferenceExpression.Empty;
 
        var expr = ReferenceExpression.CreateConditional(condition, bool.TrueString, whenTrue, whenFalse);
 
        var value = await expr.GetValueAsync(default);
        Assert.Equal(",ssl=true", value);
    }
 
    [Fact]
    public async Task GetValueAsync_ReturnsFalseValue_WhenConditionIsFalse()
    {
        var condition = new TestValueProvider(bool.FalseString);
        var whenTrue = ReferenceExpression.Create($",ssl=true");
        var whenFalse = ReferenceExpression.Empty;
 
        var expr = ReferenceExpression.CreateConditional(condition, bool.TrueString, whenTrue, whenFalse);
 
        var value = await expr.GetValueAsync(default);
        Assert.Null(value);
    }
 
    [Fact]
    public void ValueExpression_ReturnsManifestParameterReference()
    {
        var condition = new TestValueProvider(bool.TrueString);
        var whenTrue = ReferenceExpression.Create($",ssl=true");
        var whenFalse = ReferenceExpression.Empty;
 
        var expr = ReferenceExpression.CreateConditional(condition, bool.TrueString, whenTrue, whenFalse);
 
        Assert.StartsWith("{cond-", expr.ValueExpression);
        Assert.EndsWith(".connectionString}", expr.ValueExpression);
    }
 
    [Fact]
    public async Task ConditionalReferenceExpression_WorksInReferenceExpressionBuilder()
    {
        var condition = new TestValueProvider(bool.FalseString);
        var whenTrue = ReferenceExpression.Create($",ssl=true");
        var whenFalse = ReferenceExpression.Empty;
 
        var conditional = ReferenceExpression.CreateConditional(condition, bool.TrueString, whenTrue, whenFalse);
 
        var builder = new ReferenceExpressionBuilder();
        builder.AppendLiteral("localhost:6379");
        builder.Append($"{conditional}");
        var expression = builder.Build();
 
        // Before enabling: runtime value does not include TLS
        var valueBefore = await expression.GetValueAsync(new(), default);
        Assert.Equal("localhost:6379", valueBefore);
 
        // After enabling: runtime value includes TLS dynamically
        condition.Value = bool.TrueString;
        var valueAfter = await expression.GetValueAsync(new(), default);
        Assert.Equal("localhost:6379,ssl=true", valueAfter);
    }
 
    [Fact]
    public void References_IncludesConditionAndBranchReferences()
    {
        var resource = new TestResourceWithEndpoints("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var condition = endpointRef.Property(EndpointProperty.TlsEnabled);
        var whenTrue = ReferenceExpression.Create($",ssl=true");
        var whenFalse = ReferenceExpression.Empty;
 
        var expr = ReferenceExpression.CreateConditional(condition, bool.TrueString, whenTrue, whenFalse);
 
        var references = ((IValueWithReferences)expr).References.ToList();
        Assert.Contains(endpointRef, references);
    }
 
    [Fact]
    public void Name_IsAutoGeneratedFromCondition()
    {
        var expr = ReferenceExpression.CreateConditional(new TestValueProvider(bool.TrueString), bool.TrueString, ReferenceExpression.Empty, ReferenceExpression.Empty);
        Assert.StartsWith("cond-", expr.Name);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ManifestSelectedBranchRetainsParameterReferences(bool conditionValue)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var publicHost = builder.AddParameter("public-host");
        var expression = ReferenceExpression.CreateConditional(new TestValueProvider(conditionValue.ToString()), bool.TrueString,
            ReferenceExpression.Create($"https://{publicHost}/signin-oidc"),
            ReferenceExpression.Create($"http://{publicHost}/fallback"));
        builder.AddContainer("backend", "image")
            .WithEnvironment("DIRECT", expression)
            .WithEnvironment("COMPOSITE", ReferenceExpression.Create($"redirect={expression}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseParameters(conditionValue);
    }
 
    [Fact]
    public async Task ManifestSelectedBranchRetainsInfrastructureOutput()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
        var infrastructure = builder.AddResource(new AzureBicepResource("infra",
            templateString: "output publicHost string = 'public.example'"));
        var publicHost = new BicepOutputReference("publicHost", infrastructure.Resource);
        var expression = ReferenceExpression.CreateConditional(new TestValueProvider(bool.TrueString), bool.TrueString,
            ReferenceExpression.Create($"https://{publicHost}"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image").WithEnvironment("ORIGIN", expression);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>(), workspace.Path).DefaultTimeout();
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task ManifestCallbackRegistersReferencedConditionalValues()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
        var publicHost = builder.AddParameter("public-host");
        var expression = ReferenceExpression.CreateConditional(new TestValueProvider(bool.TrueString), bool.TrueString,
            ReferenceExpression.Create($"https://{publicHost}"), ReferenceExpression.Empty);
        var infrastructure = builder.AddResource(new AzureBicepResource("infra",
            templateString: "param origin string"));
        infrastructure.Resource.Parameters["origin"] = ReferenceExpression.Create($"origin={expression}");
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>(), workspace.Path);
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task ManifestConditionMustResolveDuringPublication()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var condition = new ParameterResource("condition", _ =>
            throw new InvalidOperationException("The condition is unavailable during publication."));
        var expression = ReferenceExpression.CreateConditional(condition, bool.TrueString,
            ReferenceExpression.Create($"selected-true"), ReferenceExpression.Create($"selected-false"));
        builder.AddContainer("backend", "image").WithEnvironment("RESULT", expression);
        using var app = builder.Build();
 
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() => ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.Equal("The condition is unavailable during publication.", error.Message);
    }
 
    [Fact]
    public async Task ManifestSelectedBranchReusesFormattedParameters()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var path = new ParameterResource("path", _ =>
            throw new InvalidOperationException("A deployment parameter must not be resolved during publication."));
        var expression = ReferenceExpression.CreateConditional(new TestValueProvider(bool.TrueString), bool.TrueString,
            ReferenceExpression.Create($"https://public.example/{path:uri}"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image")
            .WithEnvironment("PATH", ReferenceExpression.Create($"{path:uri}"))
            .WithEnvironment("REDIRECT", expression);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task ManifestOnlyEvaluatesSelectedNestedConditions()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var publicHost = builder.AddParameter("public-host");
        var unselectedCondition = new ParameterResource("unselected-condition", _ =>
            throw new InvalidOperationException("An unselected condition must not be evaluated."));
        var unselected = ReferenceExpression.CreateConditional(unselectedCondition, bool.TrueString,
            ReferenceExpression.Create($"unused-true"), ReferenceExpression.Create($"unused-false"));
        var selected = ReferenceExpression.CreateConditional(new TestValueProvider(bool.FalseString), bool.TrueString,
            unselected, ReferenceExpression.Create($"https://{publicHost}"));
        var expression = ReferenceExpression.CreateConditional(new TestValueProvider(bool.TrueString), bool.TrueString,
            ReferenceExpression.Create($"{selected}/signin-oidc"), unselected);
        builder.AddContainer("backend", "image").WithEnvironment("REDIRECT", expression);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    private sealed class TestResourceWithEndpoints(string name) : Resource(name), IResourceWithEndpoints
    {
    }
 
    private sealed class TestValueProvider(string value) : IValueProvider, IManifestExpressionProvider
    {
        public string Value { get; set; } = value;
 
        public string ValueExpression => "test-condition";
 
        public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default)
        {
            return new ValueTask<string?>(Value);
        }
 
        public ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default)
        {
            return new ValueTask<string?>(Value);
        }
    }
}
 
public class EndpointPropertyTlsEnabledTests
{
    [Fact]
    public async Task TlsEnabled_ReturnsFalseString_WhenTlsNotEnabled()
    {
        var resource = new TestResourceWithEndpoints("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "tcp");
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var tlsExpr = endpointRef.Property(EndpointProperty.TlsEnabled);
 
        var value = await tlsExpr.GetValueAsync();
        Assert.Equal(bool.FalseString, value);
    }
 
    [Fact]
    public async Task TlsEnabled_ReturnsTrueString_WhenTlsEnabled()
    {
        var resource = new TestResourceWithEndpoints("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "tcp")
        {
            TlsEnabled = true
        };
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var tlsExpr = endpointRef.Property(EndpointProperty.TlsEnabled);
 
        var value = await tlsExpr.GetValueAsync();
        Assert.Equal(bool.TrueString, value);
    }
 
    [Fact]
    public void ValueExpression_ReturnsEndpointTlsExpression()
    {
        var resource = new TestResourceWithEndpoints("myresource");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "tcp");
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var tlsExpr = endpointRef.Property(EndpointProperty.TlsEnabled);
 
        Assert.Equal("{myresource.bindings.tcp.tlsEnabled}", tlsExpr.ValueExpression);
    }
 
    [Fact]
    public async Task TlsEnabled_ResolvesLazilyFromCurrentState()
    {
        var resource = new TestResourceWithEndpoints("test");
        var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "tcp");
        resource.Annotations.Add(annotation);
        var endpointRef = new EndpointReference(resource, annotation);
 
        var tlsExpr = endpointRef.Property(EndpointProperty.TlsEnabled);
 
        Assert.Equal(bool.FalseString, await tlsExpr.GetValueAsync());
 
        annotation.TlsEnabled = true;
        Assert.Equal(bool.TrueString, await tlsExpr.GetValueAsync());
    }
 
    private sealed class TestResourceWithEndpoints(string name) : Resource(name), IResourceWithEndpoints
    {
    }
}