File: RadiusEnvironmentResourceTests.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.
 
#pragma warning disable ASPIRECOMPUTE002
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Radius.Tests;
 
public class RadiusEnvironmentResourceTests
{
    [Fact]
    public void Constructor_SetsName()
    {
        var resource = new RadiusEnvironmentResource("my-radius");
 
        Assert.Equal("my-radius", resource.Name);
    }
 
    [Fact]
    public void Namespace_DefaultsToDefault()
    {
        var resource = new RadiusEnvironmentResource("radius");
 
        Assert.Equal("default", resource.Namespace);
    }
 
    [Fact]
    public void Namespace_CanBeSet()
    {
        var resource = new RadiusEnvironmentResource("radius")
        {
            Namespace = "staging-ns"
        };
 
        Assert.Equal("staging-ns", resource.Namespace);
    }
 
    [Fact]
    public void ImplementsIComputeEnvironmentResource()
    {
        var resource = new RadiusEnvironmentResource("radius");
 
        Assert.IsAssignableFrom<IComputeEnvironmentResource>(resource);
    }
 
    [Fact]
    public void ImplementsIResource()
    {
        var resource = new RadiusEnvironmentResource("radius");
 
        Assert.IsAssignableFrom<IResource>(resource);
    }
 
    [Fact]
    public void GetHostAddressExpression_ReturnsKubernetesDns()
    {
        var environment = new RadiusEnvironmentResource("radius");
        var container = new ContainerResource("my-service");
        var endpoint = new EndpointReference(container, "http");
 
        var expression = ((IComputeEnvironmentResource)environment).GetHostAddressExpression(endpoint);
 
        // The Radius Kubernetes container recipe names the Service `{name}-{name}`
        // (${normalizedName}-${containerName}, both = the resource name), so service discovery must
        // address that Service. The namespace segment is required for the FQDN to resolve across
        // namespaces; the default environment namespace is "default".
        Assert.Equal("my-service-my-service.default.svc.cluster.local", expression.ValueExpression);
    }
 
    [Theory]
    [InlineData(EndpointProperty.Url, "http://api-api.default.svc.cluster.local:8080")]
    [InlineData(EndpointProperty.Host, "api-api.default.svc.cluster.local")]
    [InlineData(EndpointProperty.IPV4Host, "api-api.default.svc.cluster.local")]
    [InlineData(EndpointProperty.HostAndPort, "api-api.default.svc.cluster.local:8080")]
    [InlineData(EndpointProperty.Port, "8080")]
    [InlineData(EndpointProperty.TargetPort, "8080")]
    [InlineData(EndpointProperty.Scheme, "http")]
    public void GetEndpointPropertyExpression_UsesServiceNameAndContainerPort(EndpointProperty property, string expected)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var env = builder.AddRadiusEnvironment("radius");
        var api = builder.AddContainer("api", "myapp/api", "latest")
            .WithHttpEndpoint(targetPort: 8080, name: "http");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var radiusEnv = model.Resources.OfType<RadiusEnvironmentResource>().First();
        var endpoint = api.GetEndpoint("http");
 
        var expression = ((IComputeEnvironmentResource)radiusEnv)
            .GetEndpointPropertyExpression(endpoint.Property(property));
 
        // The recipe Service is `{name}-{name}` and listens on the container port (8080), so the
        // Url/Host/Port all target that Service and port rather than the bare name / port 80.
        Assert.Equal(expected, expression.ValueExpression);
    }
 
    /// <summary>
    /// The transport-security guard the Radius publisher applies has to hold on this path too. A
    /// Kubernetes, Azure Container Apps, or App Service consumer of a Radius-owned backing resource
    /// is routed here by <c>ComputeEnvironmentEndpointResolver</c>, and no mapped Radius type
    /// publishes a transport-security output — so answering from the local endpoint declaration
    /// would describe how the resource runs locally rather than how its recipe deploys it.
    /// </summary>
    [Theory]
    [InlineData(EndpointProperty.Scheme)]
    [InlineData(EndpointProperty.TlsEnabled)]
    [InlineData(EndpointProperty.Url)]
    public void GetEndpointPropertyExpression_TlsEnabledBackingResource_Throws(EndpointProperty property)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("radius");
        var cache = builder.AddRedis("cache").WithEndpoint("tcp", e => e.TlsEnabled = true);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var radiusEnv = model.Resources.OfType<RadiusEnvironmentResource>().First();
        var endpoint = cache.GetEndpoint("tcp");
 
        var ex = Assert.Throws<RadiusBackingResourceProjectionException>(() => ((IComputeEnvironmentResource)radiusEnv)
            .GetEndpointPropertyExpression(endpoint.Property(property)));
 
        Assert.Contains("ASPIRERADIUS081", ex.Message, StringComparison.Ordinal);
        Assert.Equal("cache", ex.Resource.Name);
    }
 
    /// <summary>
    /// The guard is scoped to transport security on a backing resource, so it must not fire for a
    /// plain container endpoint — those answer their scheme correctly on this path and did so
    /// before backing-resource projection existed.
    /// </summary>
    [Fact]
    public void GetEndpointPropertyExpression_TlsEnabledContainerEndpoint_IsAnswered()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("radius");
        var api = builder.AddContainer("api", "myapp/api", "latest")
            .WithHttpsEndpoint(targetPort: 8443, name: "https");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var radiusEnv = model.Resources.OfType<RadiusEnvironmentResource>().First();
        var endpoint = api.GetEndpoint("https");
 
        var expression = ((IComputeEnvironmentResource)radiusEnv)
            .GetEndpointPropertyExpression(endpoint.Property(EndpointProperty.Scheme));
 
        Assert.Equal("https", expression.ValueExpression);
    }
 
    /// <summary>
    /// A backing resource without TLS is unaffected: the recipe's transport is what the endpoint
    /// already describes, so there is nothing to disagree about.
    /// </summary>
    [Fact]
    public void GetEndpointPropertyExpression_BackingResourceWithoutTls_AnswersScheme()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("radius");
        var cache = builder.AddRedis("cache");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var radiusEnv = model.Resources.OfType<RadiusEnvironmentResource>().First();
        var endpoint = cache.GetEndpoint("tcp");
 
        var expression = ((IComputeEnvironmentResource)radiusEnv)
            .GetEndpointPropertyExpression(endpoint.Property(EndpointProperty.Scheme));
 
        Assert.Equal("redis", expression.ValueExpression);
    }
}