File: BicepValueProxyTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Provisioning.Tests\Aspire.Hosting.Azure.Provisioning.Tests.csproj (Aspire.Hosting.Azure.Provisioning.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.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection;
using Aspire.Hosting.ApplicationModel;
using Azure.Core;
using Azure.Provisioning;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Primitives;
using Xunit;
 
namespace Aspire.Hosting.Azure.Provisioning.Tests;
 
public class BicepValueProxyTests
{
    [Fact]
    public void AssemblyUsesProvisioningExperimentalDiagnostic()
    {
        var attribute = Assert.Single(
            typeof(BicepValueProxy).Assembly.GetCustomAttributes(typeof(ExperimentalAttribute), inherit: false)
                .Cast<ExperimentalAttribute>());
 
        Assert.Equal("ASPIREAZUREPROVISIONING001", attribute.DiagnosticId);
        Assert.Equal("https://aka.ms/aspire/diagnostics/{0}", attribute.UrlFormat);
    }
 
    [Fact]
    public void AssignToPreservesLiteralValue()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<string>("value"));
        var target = new BicepValue<string>("initial");
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal("value", assigned.LiteralValue);
    }
 
    [Fact]
    public void AssignToPreservesSecureLiteralMetadata()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<string>("secret"), isSecure: true);
        var target = new BicepValue<string>("initial");
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal("secret", assigned.LiteralValue);
    }
 
    [Fact]
    public void AssignToPreservesExpression()
    {
        var proxy = BicepValueProxy.Create(
            new BicepValue<string>(new IdentifierExpression("resourceName")));
        var target = new BicepValue<string>("initial");
 
        proxy.AssignTo(target);
 
        Assert.Equal(BicepValueKind.Expression, ((IBicepValue)target).Kind);
        Assert.Equal("resourceName", target.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void AssignToPreservesSecureMetadata()
    {
        var proxy = BicepValueProxy.Create(
            new BicepValue<string>(new IdentifierExpression("secretValue")),
            isSecure: true);
        var target = new BicepValue<string>("initial");
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("secretValue", target.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void AssignToAllowsCompatibleNullableLiteralTarget()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<int>(42));
        var target = new BicepValue<int?>(null);
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal(42, assigned.LiteralValue);
        Assert.Equal(42, target.Value);
    }
 
    [Fact]
    public void ConvertPreservesExpression()
    {
        var proxy = BicepValueProxy.Create(
            new BicepValue<int>(new BinaryExpression(20, BinaryBicepOperator.Add, 10)));
 
        var converted = BicepValueProxy.Convert<int>(proxy);
 
        Assert.Equal(BicepValueKind.Expression, ((IBicepValue)converted).Kind);
        Assert.Equal("(20 + 10)", converted.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void ConvertPreservesSecureLiteralMetadata()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<string>("secret"), isSecure: true);
 
        var converted = BicepValueProxy.Convert<string>(proxy);
 
        var assigned = (IBicepValue)converted;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal("secret", assigned.LiteralValue);
    }
 
    [Theory]
    [InlineData("192.0.2.1")]
    [InlineData("2001:db8::1")]
    public void ConvertParsesIPAddressAndRoundTripsThroughProxy(string address)
    {
        var converted = BicepValueProxy.Convert<IPAddress>(address);
        var proxy = BicepValueProxy.Create(converted, isSecure: true);
        var roundTripped = BicepValueProxy.Convert<IPAddress>(proxy);
 
        var assigned = (IBicepValue)roundTripped;
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal(IPAddress.Parse(address), assigned.LiteralValue);
        Assert.True(assigned.IsSecure);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("not-an-address")]
    [InlineData("192.0.2.999")]
    [InlineData("2001:db8::invalid")]
    public void ConvertRejectsInvalidIPAddress(string address)
    {
        Assert.Throws<FormatException>(() => BicepValueProxy.Convert<IPAddress>(address));
    }
 
    [Theory]
    [InlineData("192.0.2.1")]
    [InlineData("2001:db8::1")]
    public void ConvertParsesStringFactoryIPAddress(string address)
    {
        var proxy = CreateInfrastructure().Create().String(address);
 
        var converted = BicepValueProxy.Convert<IPAddress>(proxy);
 
        Assert.Equal(BicepValueKind.Literal, ((IBicepValue)converted).Kind);
        Assert.Equal(IPAddress.Parse(address), converted.Value);
    }
 
    [Theory]
    [InlineData("192.0.2.1")]
    [InlineData("2001:db8::1")]
    public void AssignToParsesSecureStringIPAddress(string address)
    {
        var proxy = BicepValueProxy.Create(new BicepValue<string>(address), isSecure: true);
        var target = new BicepValue<IPAddress>(IPAddress.Loopback);
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal(IPAddress.Parse(address), assigned.LiteralValue);
        Assert.True(assigned.IsSecure);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("not-an-address")]
    [InlineData("192.0.2.999")]
    [InlineData("2001:db8::invalid")]
    public void AssignToRejectsInvalidStringIPAddressWithoutChangingTarget(string address)
    {
        var proxy = CreateInfrastructure().Create().String(address);
        var target = new BicepValue<IPAddress>(IPAddress.Loopback);
 
        Assert.Throws<FormatException>(() => BicepValueProxy.Convert<IPAddress>(proxy));
        Assert.Throws<FormatException>(() => proxy.AssignTo(target));
        Assert.Equal(IPAddress.Loopback, target.Value);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void ConvertPreservesStringFactoryIPAddressExpressionAndSecurity(bool useReferenceExpression)
    {
        var factory = CreateInfrastructure().Create();
        var builder = DistributedApplication.CreateBuilder();
        var parameter = builder.AddParameter("address", "192.0.2.1", secret: true);
        var proxy = useReferenceExpression
            ? factory.ReferenceExpression(ReferenceExpression.Create($"{parameter}"), "ipAddress", isSecure: true)
            : factory.Parameter(parameter, "ipAddress");
 
        var converted = BicepValueProxy.Convert<IPAddress>(proxy);
        var roundTripped = BicepValueProxy.Convert<IPAddress>(BicepValueProxy.Create(converted));
 
        var assigned = (IBicepValue)roundTripped;
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("ipAddress", roundTripped.ToBicepExpression().ToString());
        Assert.True(assigned.IsSecure);
    }
 
    [Fact]
    public void ConvertPreservesStringIPAddressSourceReference()
    {
        var parameter = new ProvisioningParameter("ipAddress", typeof(string)) { IsSecure = true };
        BicepValue<string> source = parameter;
        var reference = new BicepValueReference(parameter, "Value");
        ((IBicepValue)source).Self = reference;
        var proxy = BicepValueProxy.Create(source);
 
        var converted = BicepValueProxy.Convert<IPAddress>(proxy);
 
        var assigned = (IBicepValue)converted;
        Assert.Same(reference, assigned.Source);
        Assert.Equal(source.ToBicepExpression().ToString(), converted.ToBicepExpression().ToString());
        Assert.True(assigned.IsSecure);
    }
 
    [Fact]
    public void ConvertPreservesIPAddressExpressionAndSecurity()
    {
        var proxy = BicepValueProxy.Create(
            new BicepValue<IPAddress>(new IdentifierExpression("ipAddress")),
            isSecure: true);
 
        var converted = BicepValueProxy.Convert<IPAddress>(proxy);
        var assigned = (IBicepValue)converted;
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("ipAddress", converted.ToBicepExpression().ToString());
        Assert.True(assigned.IsSecure);
    }
 
    [Fact]
    public void ConvertAllowsUntypedExpressionForIPAddress()
    {
        var proxy = BicepValueProxy.CreateExpression(new IdentifierExpression("ipAddress"), isSecure: false);
 
        var converted = BicepValueProxy.Convert<IPAddress>(proxy);
 
        Assert.Equal(BicepValueKind.Expression, ((IBicepValue)converted).Kind);
        Assert.Equal("ipAddress", converted.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void ConvertRejectsIncompatibleIPAddressProxy()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<int>(42));
 
        var exception = Assert.Throws<ArgumentException>(() => BicepValueProxy.Convert<IPAddress>(proxy));
 
        Assert.Equal("A literal of type Int32 cannot be assigned to a BicepValue<IPAddress>.", exception.Message);
    }
 
    [Fact]
    public void AssignToPreservesAzureLocationLiteral()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<AzureLocation>(AzureLocation.WestUS2));
        var target = new BicepValue<AzureLocation>(AzureLocation.EastUS);
 
        proxy.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.Equal(BicepValueKind.Literal, assigned.Kind);
        Assert.Equal(AzureLocation.WestUS2, assigned.LiteralValue);
    }
 
    [Fact]
    public void ConvertRejectsIncompatibleLiteralType()
    {
        var proxy = BicepValueProxy.Create(new BicepValue<int>(42));
 
        var exception = Assert.Throws<ArgumentException>(() => BicepValueProxy.Convert<string>(proxy));
 
        Assert.Equal(
            "A literal of type Int32 cannot be assigned to a BicepValue<String>.",
            exception.Message);
    }
 
    [Fact]
    public void DeclarationProxiesValidateConfiguredLiteralType()
    {
        var infrastructure = CreateInfrastructure();
        var parameter = infrastructure.AddBicepParameter("parameter", ProvisioningValueType.Integer);
        var output = infrastructure.AddBicepOutput("output", ProvisioningValueType.Integer);
        var variable = infrastructure.AddBicepVariable("variable", ProvisioningValueType.Integer);
 
        AssertDeclarationTypeValidation(
            value => parameter.Value = value,
            () => parameter.Inner.Value);
        AssertDeclarationTypeValidation(
            value => output.Value = value,
            () => output.Inner.Value);
        AssertDeclarationTypeValidation(
            value => variable.Value = value,
            () => variable.Inner.Value);
    }
 
    [Fact]
    public void DeclarationProxiesPreserveConfiguredTypeWhenRead()
    {
        var infrastructure = CreateInfrastructure();
        var parameter = infrastructure.AddBicepParameter("parameter", ProvisioningValueType.Integer);
        var output = infrastructure.AddBicepOutput("output", ProvisioningValueType.Integer);
        var variable = infrastructure.AddBicepVariable("variable", ProvisioningValueType.Integer);
 
        AssertDeclarationTypeRoundTrips(
            value => parameter.Value = value,
            () => parameter.Value);
        AssertDeclarationTypeRoundTrips(
            value => output.Value = value,
            () => output.Value);
        AssertDeclarationTypeRoundTrips(
            value => variable.Value = value,
            () => variable.Value);
    }
 
    [Theory]
    [InlineData(nameof(ProvisioningValueType.Boolean))]
    [InlineData(nameof(ProvisioningValueType.Integer))]
    public void BicepParametersRejectUnsupportedSecureTypes(string typeName)
    {
        var infrastructure = CreateInfrastructure();
        var type = Enum.Parse<ProvisioningValueType>(typeName);
        var originalResources = infrastructure.GetProvisionableResources().ToArray();
 
        var creationException = Assert.Throws<ArgumentException>(
            () => infrastructure.AddBicepParameter("parameter", type, isSecure: true));
 
        Assert.Equal("isSecure", creationException.ParamName);
        Assert.Equal(
            "Only string, object, and GUID Bicep parameters can be secure. (Parameter 'isSecure')",
            creationException.Message);
        Assert.Equal(originalResources, infrastructure.GetProvisionableResources());
 
        var parameter = infrastructure.AddBicepParameter("parameter", type, isSecure: false);
        Assert.False(parameter.IsSecure);
 
        var mutationException = Assert.Throws<ArgumentException>(() => parameter.IsSecure = true);
 
        Assert.Equal("value", mutationException.ParamName);
        Assert.False(parameter.IsSecure);
        Assert.False(parameter.Inner.IsSecure);
 
        parameter.IsSecure = false;
        Assert.False(parameter.IsSecure);
    }
 
    [Theory]
    [InlineData(nameof(ProvisioningValueType.String))]
    [InlineData(nameof(ProvisioningValueType.Object))]
    [InlineData(nameof(ProvisioningValueType.Guid))]
    public void BicepParametersSupportSecureTypes(string typeName)
    {
        var infrastructure = CreateInfrastructure();
        var type = Enum.Parse<ProvisioningValueType>(typeName);
        var parameter = infrastructure.AddBicepParameter("parameter", type, isSecure: true);
 
        Assert.True(parameter.IsSecure);
        Assert.True(parameter.Inner.IsSecure);
 
        parameter.IsSecure = false;
        Assert.False(parameter.IsSecure);
        Assert.False(parameter.Inner.IsSecure);
 
        parameter.IsSecure = true;
        Assert.True(parameter.IsSecure);
        Assert.True(parameter.Inner.IsSecure);
    }
 
    [Theory]
    [InlineData(nameof(ProvisioningValueType.Boolean), true)]
    [InlineData(nameof(ProvisioningValueType.Integer), 42)]
    public void BicepParametersRejectUnsupportedSecureValues(string typeName, object literal)
    {
        var parameter = CreateInfrastructure().AddBicepParameter(
            "parameter", Enum.Parse<ProvisioningValueType>(typeName));
        IBicepValue initialValue = new BicepValue<object>(literal);
        parameter.Value = BicepValueProxy.Create(initialValue);
        var secureValue = literal switch
        {
            bool value => BicepValueProxy.Create(new BicepValue<bool>(value), isSecure: true),
            int value => BicepValueProxy.Create(new BicepValue<int>(value), isSecure: true),
            _ => throw new ArgumentException("Expected a Boolean or integer literal.", nameof(literal))
        };
 
        var exception = Assert.Throws<ArgumentException>(() => parameter.Value = secureValue);
 
        Assert.Equal("value", exception.ParamName);
        Assert.Equal(literal, ((IBicepValue)parameter.Inner.Value).LiteralValue);
        Assert.False(parameter.IsSecure);
        Assert.False(parameter.Value.IsSecure);
    }
 
    [Fact]
    public void InterpolatedValuePreservesExpressionAndSecurity()
    {
        var secureValue = BicepValueProxy.Create(
            new BicepValue<string>(new IdentifierExpression("secretValue")),
            isSecure: true);
        var builder = new BicepStringBuilderProxy()
            .AppendLiteral("prefix-")
            .AppendValue(secureValue);
 
        var result = builder.Build();
        var target = new BicepValue<string>("initial");
        result.AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("'prefix-${secretValue}'", target.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void ParameterFactoryCreatesSecureParameterReference()
    {
        var builder = DistributedApplication.CreateBuilder();
        var parameter = builder.AddParameter("secret", "value", secret: true);
        var target = new BicepValue<string>("initial");
 
        CreateInfrastructure()
            .Create()
            .Parameter(parameter, "secretParam")
            .AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("secretParam", target.ToBicepExpression().ToString());
    }
 
    [Fact]
    public void ReferenceExpressionFactoryCreatesSecureParameterReference()
    {
        var target = new BicepValue<string>("initial");
 
        CreateInfrastructure()
            .Create()
            .ReferenceExpression(ReferenceExpression.Create($"secretValue"), "secretParam", isSecure: true)
            .AssignTo(target);
 
        var assigned = (IBicepValue)target;
        Assert.True(assigned.IsSecure);
        Assert.Equal(BicepValueKind.Expression, assigned.Kind);
        Assert.Equal("secretParam", target.ToBicepExpression().ToString());
    }
 
    private static AzureResourceInfrastructure CreateInfrastructure()
    {
        var constructor = typeof(AzureResourceInfrastructure).GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            binder: null,
            [typeof(AzureProvisioningResource), typeof(string)],
            modifiers: null);
 
        Assert.NotNull(constructor);
 
        return (AzureResourceInfrastructure)constructor.Invoke([new AzureProvisioningResource("test", _ => { }), "test"]);
    }
 
    private static void AssertDeclarationTypeValidation(
        Action<BicepValueProxy> assign,
        Func<IBicepValue> getAssignedValue)
    {
        assign(BicepValueProxy.Create(new BicepValue<int>(42)));
        Assert.Equal(42, getAssignedValue().LiteralValue);
 
        var exception = Assert.Throws<ArgumentException>(
            () => assign(BicepValueProxy.Create(new BicepValue<string>("text"))));
 
        Assert.Equal(
            "A literal of type String cannot be assigned to a BicepValue<Int32>.",
            exception.Message);
        Assert.Equal(42, getAssignedValue().LiteralValue);
    }
 
    private static void AssertDeclarationTypeRoundTrips(
        Action<BicepValueProxy> assign,
        Func<BicepValueProxy> read)
    {
        assign(BicepValueProxy.Create(new BicepValue<int>(42)));
 
        var literalTarget = new BicepValue<int>(0);
        read().AssignTo(literalTarget);
        Assert.Equal(42, literalTarget.Value);
 
        assign(BicepValueProxy.Create(
            new BicepValue<int>(new IdentifierExpression("integerValue"))));
 
        var expressionTarget = new BicepValue<int>(0);
        read().AssignTo(expressionTarget);
        Assert.Equal(BicepValueKind.Expression, ((IBicepValue)expressionTarget).Kind);
        Assert.Equal("integerValue", expressionTarget.ToBicepExpression().ToString());
 
        var exception = Assert.Throws<ArgumentException>(
            () => read().AssignTo(new BicepValue<string>("initial")));
 
        Assert.Equal(
            "A value of type Int32 cannot be assigned to a BicepValue<String>.",
            exception.Message);
    }
}