File: Publishing\BicepIdentifierTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Radius.Tests\Aspire.Hosting.Radius.Tests.csproj (Aspire.Hosting.Radius.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Radius.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Radius.Tests.Publishing;
 
public class BicepIdentifierTests
{
    [Theory]
    [InlineData("cache", "cache")]
    [InlineData("my-cache", "my_cache")]
    [InlineData("my.cache", "my_cache")]
    [InlineData("my cache", "my_cache")]
    [InlineData("123start", "r123start")]
    [InlineData("", "resource")]
    [InlineData("valid_name", "valid_name")]
    [InlineData("CamelCase", "CamelCase")]
    public void SanitizeIdentifier_ProducesValidBicepIdentifiers(string input, string expected)
    {
        var result = BicepPostProcessor.SanitizeIdentifier(input);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void SanitizeIdentifier_RadiusName_SanitizedToRadiusenv()
    {
        // "radius" collides with `extension radius` directive
        var result = BicepPostProcessor.SanitizeIdentifier("radius");
        Assert.Equal("radiusenv", result);
    }
 
    [Fact]
    public void SanitizeIdentifier_RadiusCaseInsensitive()
    {
        Assert.Equal("radiusenv", BicepPostProcessor.SanitizeIdentifier("Radius"));
        Assert.Equal("radiusenv", BicepPostProcessor.SanitizeIdentifier("RADIUS"));
    }
 
    [Fact]
    public void HyphenatedResourceNames_HandledInBicep()
    {
        // When a resource name contains hyphens, the SDK handles quoting automatically
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("myenv");
        var cache = builder.AddRedis("my-cache");
        builder.AddContainer("api", "myapp/api", "latest")
            .WithReference(cache);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var radiusEnv = model.Resources.OfType<RadiusEnvironmentResource>().First();
        RadiusTestHelper.AttachDeploymentTargets(radiusEnv, model);
        var context = new RadiusBicepPublishingContext(radiusEnv);
        var bicep = context.GenerateBicep(model);
 
        // The connection key for a hyphenated name should be present in the output
        Assert.Contains("my-cache", bicep);
    }
 
    [Theory]
    [InlineData("app")]
    [InlineData("recipepack")]
    public void ResourceNameCollidingWithSynthesizedIdentifier_Throws_ASPIRERADIUS056(string reservedName)
    {
        // The publisher synthesizes constructs with the fixed Bicep identifiers 'app' and
        // 'recipepack'. A backing resource whose (sanitized) name equals one of these produces two
        // declarations with the same symbol; the SDK's Compile() does not detect this, so fail fast.
        // A container is added to force the UDT chain (application + recipe pack) to be emitted.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var env = builder.AddRadiusEnvironment("myenv");
        builder.AddRedis(reservedName);
        builder.AddContainer("api", "myapp/api", "latest");
 
        var ex = GenerateAndCaptureException(builder, env);
 
        Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("ASPIRERADIUS056", ex!.Message);
        Assert.Contains(reservedName, ex.Message);
    }
 
    [Fact]
    public void NormalGraph_GeneratesBicep_WithoutCollision()
    {
        // Control: distinct backing resources plus a container (forcing the UDT chain, so 'app'
        // and 'recipepack' are also emitted) must compile without an ASPIRERADIUS056 collision.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var env = builder.AddRadiusEnvironment("myenv");
        builder.AddRedis("cache");
        builder.AddContainer("api", "myapp/api", "latest");
 
        var ex = GenerateAndCaptureException(builder, env);
 
        Assert.Null(ex);
    }
 
    private static Exception? GenerateAndCaptureException(
        IDistributedApplicationBuilder builder,
        IResourceBuilder<RadiusEnvironmentResource> env)
    {
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        RadiusTestHelper.AttachDeploymentTargets(env.Resource, model);
        var context = new RadiusBicepPublishingContext(env.Resource);
        return Record.Exception(() => context.GenerateBicep(model));
    }
}