| File: RadiusExtensionsTests.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 ASPIRERADIUS003 // WithContainerImage is marked experimental; opt in for tests. using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.Radius.Tests; public class RadiusExtensionsTests { [Fact] public void AddRadiusEnvironment_CreatesResource_WithDefaults() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); Assert.NotNull(resourceBuilder); Assert.Equal("radius", resourceBuilder.Resource.Name); Assert.Equal("default", resourceBuilder.Resource.Namespace); } [Fact] public void AddRadiusEnvironment_WithCustomName() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("staging"); Assert.Equal("staging", resourceBuilder.Resource.Name); } [Fact] public void AddRadiusEnvironment_AddsResourceToModel() { // The Run-mode short-circuit in AddRadiusEnvironment intentionally does not register // the environment with the application builder (matching the Kubernetes integration), // so this test runs the AppHost in Publish mode to verify the resource is registered // on the publish path. using var builder = Aspire.Hosting.Utils.TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); builder.AddRadiusEnvironment("radius"); using var app = builder.Build(); var model = app.Services.GetRequiredService<DistributedApplicationModel>(); Assert.Single(model.Resources.OfType<RadiusEnvironmentResource>()); } [Fact] public void WithNamespace_SetsNamespace() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius") .WithNamespace("staging-ns"); Assert.Equal("staging-ns", resourceBuilder.Resource.Namespace); } [Fact] public void WithNamespace_ValidatesRFC1123_ThrowsOnInvalid() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); Assert.Throws<ArgumentException>(() => resourceBuilder.WithNamespace("Invalid_Namespace")); } [Fact] public void WithNamespace_ThrowsOnLeadingHyphen() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); Assert.Throws<ArgumentException>(() => resourceBuilder.WithNamespace("-invalid")); } [Fact] public void WithNamespace_ThrowsOnTrailingHyphen() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); Assert.Throws<ArgumentException>(() => resourceBuilder.WithNamespace("invalid-")); } [Fact] public void WithNamespace_ThrowsOnUppercase() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); Assert.Throws<ArgumentException>(() => resourceBuilder.WithNamespace("MyNamespace")); } [Fact] public void WithNamespace_ThrowsOnTooLong() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); var longName = new string('a', 64); Assert.Throws<ArgumentException>(() => resourceBuilder.WithNamespace(longName)); } [Fact] public void WithNamespace_AcceptsValidNames() { var builder = DistributedApplication.CreateBuilder(); var resourceBuilder = builder.AddRadiusEnvironment("radius"); // Single char resourceBuilder.WithNamespace("a"); Assert.Equal("a", resourceBuilder.Resource.Namespace); // Hyphens in middle resourceBuilder.WithNamespace("my-ns"); Assert.Equal("my-ns", resourceBuilder.Resource.Namespace); // All numbers resourceBuilder.WithNamespace("123"); Assert.Equal("123", resourceBuilder.Resource.Namespace); // Max length var maxName = new string('a', 63); resourceBuilder.WithNamespace(maxName); Assert.Equal(maxName, resourceBuilder.Resource.Namespace); } [Fact] public void AddRadiusEnvironment_ThrowsOnNullBuilder() { IDistributedApplicationBuilder builder = null!; Assert.Throws<ArgumentNullException>(() => builder.AddRadiusEnvironment("radius")); } [Fact] public void AddRadiusEnvironment_ThrowsOnEmptyName() { var builder = DistributedApplication.CreateBuilder(); Assert.Throws<ArgumentException>(() => builder.AddRadiusEnvironment("")); } // The cases below pin the documented behaviour of RadiusExtensions.ParseImageReference // (private), which is exercised through WithContainerImage. The heuristic for splitting // the registry segment from a Docker Hub user namespace (contains '.' or ':' or equals // "localhost") matches Docker/containerd, and we own the documented examples in the // RadiusExtensions.ParseImageReference summary comment — keep this table in sync if // those docs change. [Theory] [InlineData("redis", null, "redis", "latest")] [InlineData("redis:7", null, "redis", "7")] [InlineData("library/redis:7", null, "library/redis", "7")] [InlineData("localhost:5001/api:latest", "localhost:5001", "api", "latest")] [InlineData("ghcr.io/owner/repo:v1", "ghcr.io", "owner/repo", "v1")] public void WithContainerImage_ParsesImageReference( string image, string? expectedRegistry, string expectedImage, string expectedTag) { using var builder = Aspire.Hosting.Utils.TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); var project = builder.AddProject<ParseImageReferenceProjectMetadata>("webapp"); project.WithContainerImage(image); var annotation = project.Resource.Annotations.OfType<ContainerImageAnnotation>().Single(); Assert.Equal(expectedRegistry, annotation.Registry); Assert.Equal(expectedImage, annotation.Image); Assert.Equal(expectedTag, annotation.Tag); } [Fact] public void WithContainerImage_ReplacesExistingAnnotation() { // The documented contract on WithContainerImage is that a second call overrides the // first (matches the LastOrDefault() lookup the publisher uses). Pin that here too. using var builder = Aspire.Hosting.Utils.TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); var project = builder.AddProject<ParseImageReferenceProjectMetadata>("webapp"); project.WithContainerImage("redis:6"); project.WithContainerImage("ghcr.io/owner/repo:v2"); var annotation = project.Resource.Annotations.OfType<ContainerImageAnnotation>().Single(); Assert.Equal("ghcr.io", annotation.Registry); Assert.Equal("owner/repo", annotation.Image); Assert.Equal("v2", annotation.Tag); } private sealed class ParseImageReferenceProjectMetadata : IProjectMetadata { public string ProjectPath => "testproject"; public LaunchSettings LaunchSettings { get; } = new(); } }