File: Utils\VolumeNameGeneratorTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Security.Cryptography;
using System.Text;
using Aspire.Hosting.Utils;
using static Aspire.Hosting.VolumeNameGenerator;
 
namespace Aspire.Hosting.Tests.Utils;
 
public class VolumeNameGeneratorTests
{
    [Fact]
    public void VolumeGeneratorUsesUniqueName()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var volumePrefix = $"{Sanitize(builder.Environment.ApplicationName).ToLowerInvariant()}-{builder.Configuration["AppHost:Sha256"]!.ToLowerInvariant()[..10]}";
 
        var resource = builder.AddResource(new TestResource("myresource"));
 
        var volumeName = Generate(resource, "data");
 
        Assert.Equal($"{volumePrefix}-{resource.Resource.Name}-data", volumeName);
    }
 
    [Theory]
    [MemberData(nameof(InvalidNameParts))]
    public void ThrowsWhenSuffixContainsInvalidChars(string suffix)
    {
        var builder = DistributedApplication.CreateBuilder();
        var resource = builder.AddResource(new TestResource("myresource"));
 
        Assert.Throws<ArgumentException>(nameof(suffix), () => Generate(resource, suffix));
    }
 
    public static object[][] InvalidNameParts => [
        ["This/is/invalid"],
        [@"This\is\invalid"],
        ["_ThisIsInvalidToo"],
        [".ThisIsInvalidToo"],
        ["-ThisIsInvalidToo"],
        ["This&IsInvalidToo"]
    ];
 
    private sealed class TestResource(string name) : IResource
    {
        public string Name { get; } = name;
 
        public ResourceAnnotationCollection Annotations { get; } = [];
    }
 
    [Fact]
    public void VolumeNameDiffersBetweenPublishAndRun()
    {
        var runBuilder = TestDistributedApplicationBuilder.Create();
        var publishBuilder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var runVolumePrefix = $"{Sanitize(runBuilder.Environment.ApplicationName).ToLowerInvariant()}-{runBuilder.Configuration["AppHost:Sha256"]!.ToLowerInvariant()[..10]}";
        var publishVolumePrefix = $"{Sanitize(publishBuilder.Environment.ApplicationName).ToLowerInvariant()}-{publishBuilder.Configuration["AppHost:Sha256"]!.ToLowerInvariant()[..10]}";
 
        var runResource = runBuilder.AddResource(new TestResource("myresource"));
        var publishResource = publishBuilder.AddResource(new TestResource("myresource"));
 
        var runVolumeName = Generate(runResource, "data");
        var publishVolumeName = Generate(publishResource, "data");
 
        Assert.Equal($"{runVolumePrefix}-{runResource.Resource.Name}-data", runVolumeName);
        Assert.Equal($"{publishVolumePrefix}-{publishResource.Resource.Name}-data", publishVolumeName);
        Assert.NotEqual(runVolumeName, publishVolumeName);
    }
 
    [Theory]
    [InlineData(@"C:\Project\App")]
    [InlineData(@"c:\project\app")]
    [InlineData(@"C:/Project/App")]
    [InlineData(@"C:\Project\App\")]
    [InlineData(@"C:\Project\..\Project\App")]
    public void VolumeNameConsistentAcrossWindowsPathCasingsAndFormats(string projectDirectory)
    {
        if (!OperatingSystem.IsWindows())
        {
            return;
        }
 
        SharedVolumeNameConsistencyTest(projectDirectory);
    }
 
    [Theory]
    [InlineData("/home/project/app")]
    [InlineData("/home/Project/App")]
    [InlineData("/home/project/app/")]
    [InlineData("/home/project/../project/app")]
    [InlineData("./project/app")]
    public void VolumeNameConsistentAcrossLinuxPathFormats(string projectDirectory)
    {
        if (OperatingSystem.IsWindows())
        {
            return;
        }
 
        SharedVolumeNameConsistencyTest(projectDirectory);
    }
 
    private static void SharedVolumeNameConsistencyTest(string projectDirectory)
    {
        var options = new DistributedApplicationOptions
        {
            ProjectDirectory = projectDirectory,
            Args = [] // Ensure run mode (default)
        };
 
        var builder = (DistributedApplicationBuilder)DistributedApplication.CreateBuilder(options);
 
        // Verify this is in run mode so path-based hashing is used
        Assert.False(builder.ExecutionContext.IsPublishMode);
        Assert.True(builder.ExecutionContext.IsRunMode);
 
        var appHostSha = builder.Configuration["AppHost:Sha256"];
        Assert.NotNull(appHostSha);
 
        // Create a resource and verify volume name generation works
        var resource = builder.AddResource(new TestResource("myresource"));
        var volumeName = Generate(resource, "data");
        Assert.NotNull(volumeName);
        Assert.Contains("myresource-data", volumeName);
 
        // Compute expected SHA from normalized AppHostPath
        var expectedShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(builder.AppHostPath.ToLowerInvariant()));
        var expectedSha = Convert.ToHexString(expectedShaBytes);
 
        // The SHA from the configuration should match the SHA computed from normalized AppHostPath
        Assert.Equal(expectedSha, appHostSha);
    }
}