// 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
{
}
}