File: Scripts\ReleaseScriptPSFunctionTests.cs
Web Access
Project: src\tests\Aspire.Acquisition.Tests\Aspire.Acquisition.Tests.csproj (Aspire.Acquisition.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.TestUtilities;
using Xunit;
 
namespace Aspire.Acquisition.Tests.Scripts;
 
/// <summary>
/// Tier-1 unit tests for individual functions in the release PowerShell script (get-aspire-cli.ps1).
/// Tests URL construction, quality mapping, checksum validation, and archive extraction.
/// </summary>
[RequiresTools(["pwsh"])]
public class ReleaseScriptPSFunctionTests(ITestOutputHelper testOutput)
{
    private static readonly string s_releaseScript = ScriptPaths.ReleasePowerShell;
 
    private readonly ITestOutputHelper _testOutput = testOutput;
 
    #region ConvertTo-ChannelName
 
    [Theory]
    [InlineData("release", "stable")]
    [InlineData("staging", "staging")]
    [InlineData("dev", "daily")]
    public async Task ConvertToChannelName_KnownQualities_ReturnsMappedChannel(string quality, string expectedChannel)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"ConvertTo-ChannelName -Quality '{quality}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedChannel, result.Output.Trim());
    }
 
    [Fact]
    public async Task ConvertToChannelName_UnknownQuality_ReturnsAsIs()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            "ConvertTo-ChannelName -Quality 'custom-channel'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal("custom-channel", result.Output.Trim());
    }
 
    #endregion
 
    #region Get-AspireCliUrl
 
    [Theory]
    [InlineData("release", "linux-x64", "tar.gz", "https://aka.ms/dotnet/9/aspire/ga/daily/aspire-cli-linux-x64.tar.gz")]
    [InlineData("dev", "linux-x64", "tar.gz", "https://aka.ms/dotnet/9/aspire/daily/aspire-cli-linux-x64.tar.gz")]
    [InlineData("staging", "linux-x64", "tar.gz", "https://aka.ms/dotnet/9/aspire/rc/daily/aspire-cli-linux-x64.tar.gz")]
    [InlineData("release", "osx-arm64", "tar.gz", "https://aka.ms/dotnet/9/aspire/ga/daily/aspire-cli-osx-arm64.tar.gz")]
    [InlineData("release", "win-x64", "zip", "https://aka.ms/dotnet/9/aspire/ga/daily/aspire-cli-win-x64.zip")]
    public async Task GetAspireCliUrl_NoVersion_ReturnsAkaMsUrl(
        string quality, string rid, string ext, string expectedUrl)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Quality '{quality}' -RuntimeIdentifier '{rid}' -Extension '{ext}').ArchiveUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedUrl, result.Output.Trim());
    }
 
    [Theory]
    [InlineData("release", "linux-x64", "tar.gz", "https://aka.ms/dotnet/9/aspire/ga/daily/aspire-cli-linux-x64.tar.gz.sha512")]
    [InlineData("dev", "osx-arm64", "tar.gz", "https://aka.ms/dotnet/9/aspire/daily/aspire-cli-osx-arm64.tar.gz.sha512")]
    public async Task GetAspireCliUrl_NoVersionChecksum_ReturnsChecksumUrl(
        string quality, string rid, string ext, string expectedUrl)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Quality '{quality}' -RuntimeIdentifier '{rid}' -Extension '{ext}').ChecksumUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedUrl, result.Output.Trim());
    }
 
    [Theory]
    [InlineData("13.3.5", "linux-x64", "tar.gz", "https://github.com/microsoft/aspire/releases/download/v13.3.5/aspire-cli-linux-x64-13.3.5.tar.gz")]
    [InlineData("v13.3.5", "win-x64", "zip", "https://github.com/microsoft/aspire/releases/download/v13.3.5/aspire-cli-win-x64-13.3.5.zip")]
    public async Task GetAspireCliUrl_WithStableVersion_ReturnsGitHubReleaseUrl(
        string version, string rid, string ext, string expectedUrl)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Version '{version}' -Quality 'release' -RuntimeIdentifier '{rid}' -Extension '{ext}').ArchiveUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedUrl, result.Output.Trim());
    }
 
    [Theory]
    [InlineData("13.3.5", "linux-x64", "tar.gz", "https://github.com/microsoft/aspire/releases/download/v13.3.5/aspire-cli-linux-x64-13.3.5.tar.gz.sha512")]
    [InlineData("v13.3.5", "win-x64", "zip", "https://github.com/microsoft/aspire/releases/download/v13.3.5/aspire-cli-win-x64-13.3.5.zip.sha512")]
    public async Task GetAspireCliUrl_WithStableVersionChecksum_ReturnsGitHubReleaseChecksumUrl(
        string version, string rid, string ext, string expectedUrl)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Version '{version}' -Quality 'release' -RuntimeIdentifier '{rid}' -Extension '{ext}').ChecksumUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedUrl, result.Output.Trim());
    }
 
    [Theory]
    [InlineData("13.3.0-dev")]
    [InlineData("13.3.0-preview.1.25366.3")]
    [InlineData("13.3.0-local")]
    public async Task GetAspireCliUrl_WithPrereleaseVersion_ReturnsCiDotNetUrl(string version)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Version '{version}' -Quality 'release' -RuntimeIdentifier 'linux-x64' -Extension 'tar.gz').ArchiveUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal($"https://ci.dot.net/public/aspire/{version}/aspire-cli-linux-x64-{version}.tar.gz", result.Output.Trim());
    }
 
    [Theory]
    [InlineData("13.3.0-dev")]
    [InlineData("13.3.0-preview.1.25366.3")]
    [InlineData("13.3.0-local")]
    public async Task GetAspireCliUrl_WithPrereleaseVersionChecksum_ReturnsCiDotNetChecksumUrl(string version)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"(Get-AspireCliUrl -Version '{version}' -Quality 'release' -RuntimeIdentifier 'linux-x64' -Extension 'tar.gz').ChecksumUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal($"https://ci.dot.net/public-checksums/aspire/{version}/aspire-cli-linux-x64-{version}.tar.gz.sha512", result.Output.Trim());
    }
 
    [Fact]
    public async Task GetAspireCliUrl_UnsupportedQualityNoVersion_ReturnsError()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            "(Get-AspireCliUrl -Quality 'invalid' -RuntimeIdentifier 'linux-x64' -Extension 'tar.gz').ArchiveUrl",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Unsupported", result.Output, StringComparison.OrdinalIgnoreCase);
    }
 
    #endregion
 
    #region Get-DownloadDescriptor
 
    [Theory]
    [InlineData("https://aka.ms/dotnet/9/aspire/ga/daily/aspire-cli-linux-x64.tar.gz", "the stable channel", "aspire-cli-linux-x64.tar.gz from the stable channel")]
    [InlineData("https://aka.ms/dotnet/9/aspire/daily/aspire-cli-linux-x64.tar.gz", "the daily channel", "aspire-cli-linux-x64.tar.gz from the daily channel")]
    [InlineData("https://aka.ms/dotnet/9/aspire/rc/daily/aspire-cli-linux-x64.tar.gz", "the staging channel", "aspire-cli-linux-x64.tar.gz from the staging channel")]
    public async Task GetDownloadDescriptor_WithSource_ReturnsFriendlyChannelSource(string url, string source, string expectedDescriptor)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Get-DownloadDescriptor -Uri '{url}' -Source '{source}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var descriptor = result.Output.Trim();
        Assert.Equal(expectedDescriptor, descriptor);
        Assert.DoesNotContain("dotnet", descriptor, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("ga/daily", descriptor, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("rc/daily", descriptor, StringComparison.OrdinalIgnoreCase);
    }
 
    #endregion
 
    #region Test-FileChecksum
 
    [Fact]
    public async Task TestFileChecksum_MatchingChecksum_Succeeds()
    {
        using var env = new TestEnvironment();
 
        var archive = await FakeArchiveHelper.CreateFakeArchiveAsync(env.TempDirectory);
 
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Test-FileChecksum -ArchiveFile '{archive.ArchivePath}' -ChecksumFile '{archive.ChecksumPath}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
    }
 
    [Fact]
    public async Task TestFileChecksum_MismatchedChecksum_Fails()
    {
        using var env = new TestEnvironment();
 
        var archive = await FakeArchiveHelper.CreateFakeArchiveWithBadChecksumAsync(env.TempDirectory);
 
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Test-FileChecksum -ArchiveFile '{archive.ArchivePath}' -ChecksumFile '{archive.ChecksumPath}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Checksum validation failed", result.Output, StringComparison.OrdinalIgnoreCase);
    }
 
    #endregion
 
    #region Expand-AspireCliArchive
 
    [Fact]
    public async Task ExpandAspireCliArchive_TarGz_ExtractsToDestination()
    {
        using var env = new TestEnvironment();
 
        var archive = await FakeArchiveHelper.CreateFakeArchiveAsync(env.TempDirectory, "linux-x64");
        var destPath = Path.Combine(env.TempDirectory, "install-dest");
 
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Expand-AspireCliArchive -ArchiveFile '{archive.ArchivePath}' -DestinationPath '{destPath}' -OS 'linux'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.True(File.Exists(Path.Combine(destPath, "aspire")),
            "Extracted binary should exist at destination");
    }
 
    [Fact]
    public async Task ExpandAspireCliArchive_Zip_ExtractsToDestination()
    {
        using var env = new TestEnvironment();
 
        var archive = await FakeArchiveHelper.CreateFakeArchiveAsync(env.TempDirectory, "win-x64");
        var destPath = Path.Combine(env.TempDirectory, "install-dest-zip");
 
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Expand-AspireCliArchive -ArchiveFile '{archive.ArchivePath}' -DestinationPath '{destPath}' -OS 'win'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.True(File.Exists(Path.Combine(destPath, "aspire.exe")),
            "Extracted binary should exist at destination");
    }
 
    #endregion
 
    #region Get-OperatingSystem
 
    [Fact]
    public async Task GetOperatingSystem_ReturnsKnownPlatform()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            "Get-OperatingSystem",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var os = result.Output.Trim();
        Assert.True(
            os is "osx" or "linux" or "linux-musl" or "win",
            $"Expected a recognized OS, got: '{os}'");
    }
 
    #endregion
 
    #region Get-MachineArchitecture
 
    [Fact]
    public async Task GetMachineArchitecture_ReturnsKnownArch()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            "Get-MachineArchitecture",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var arch = result.Output.Trim();
        // Get-MachineArchitecture returns raw OS values:
        // Windows: AMD64/amd64/arm64, Unix (modern PS): x64/arm64
        Assert.True(
            arch.Equals("x64", StringComparison.OrdinalIgnoreCase)
            || arch.Equals("amd64", StringComparison.OrdinalIgnoreCase)
            || arch.Equals("arm64", StringComparison.OrdinalIgnoreCase),
            $"Expected x64, amd64, or arm64, got: '{arch}'");
    }
 
    #endregion
 
    #region Get-CLIArchitectureFromArchitecture
 
    [Theory]
    [InlineData("amd64", "x64")]
    [InlineData("x64", "x64")]
    [InlineData("arm64", "arm64")]
    [InlineData("AMD64", "x64")]
    [InlineData("X64", "x64")]
    [InlineData("ARM64", "arm64")]
    public async Task GetCLIArchitectureFromArchitecture_NormalizesArchNames(string input, string expected)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Get-CLIArchitectureFromArchitecture -Architecture '{input}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expected, result.Output.Trim());
    }
 
    [Fact]
    public async Task GetCLIArchitectureFromArchitecture_UnsupportedArch_Fails()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            "Get-CLIArchitectureFromArchitecture -Architecture 'mips'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("not supported", result.Output, StringComparison.OrdinalIgnoreCase);
    }
 
    #endregion
 
    #region Invoke-AspireCliBundleSetup
 
    [Fact]
    public async Task InvokeAspireCliBundleSetup_InvokesSetupAndPropagatesFailure()
    {
        using var env = new TestEnvironment();
        var cliPath = Path.Combine(env.TempDirectory, OperatingSystem.IsWindows() ? "aspire.cmd" : "aspire");
        var capturePath = Path.Combine(env.TempDirectory, "setup-args.txt");
        var contents = OperatingSystem.IsWindows()
            ? $"""
                @echo off
                > "{capturePath}" echo %~1
                exit /b 23
                """
            : $$"""
                #!/bin/sh
                printf '%s\n' "$@" > "{{capturePath}}"
                exit 23
                """;
        await File.WriteAllTextAsync(cliPath, contents.ReplaceLineEndings(OperatingSystem.IsWindows() ? "\r\n" : "\n"));
        if (!OperatingSystem.IsWindows())
        {
            File.SetUnixFileMode(cliPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        }
 
        var escapedCliPath = cliPath.Replace("'", "''");
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Invoke-AspireCliBundleSetup -CliPath '{escapedCliPath}' -TargetOS (Get-OperatingSystem) -TargetArchitecture (Get-CLIArchitectureFromArchitecture '<auto>')",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Equal("setup", (await File.ReadAllTextAsync(capturePath)).Trim());
        Assert.Contains("bundle setup failed", result.Output, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("23", result.Output);
    }
 
    [Fact]
    public async Task InvokeAspireCliBundleSetup_UnsupportedHostArchitecture_SkipsSetup()
    {
        using var env = new TestEnvironment();
        var cliPath = Path.Combine(env.TempDirectory, "setup-invoked.txt");
        var escapedCliPath = cliPath.Replace("'", "''");
        var expression = $$"""
            function Get-MachineArchitecture { 'mips' }
            Invoke-AspireCliBundleSetup -CliPath '{{escapedCliPath}}' -TargetOS (Get-OperatingSystem) -TargetArchitecture 'x64'
            """;
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            expression,
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Contains("Skipping Aspire CLI bundle setup because the current platform could not be detected", result.Output);
        Assert.Contains("not supported", result.Output, StringComparison.OrdinalIgnoreCase);
        Assert.False(File.Exists(cliPath));
    }
 
    #endregion
 
    #region Get-AspireExtensionUrl
 
    [Theory]
    [InlineData("dev", "https://aka.ms/dotnet/9/aspire/daily/aspire-vscode.vsix.zip")]
    [InlineData("staging", "https://aka.ms/dotnet/9/aspire/rc/daily/aspire-vscode.vsix.zip")]
    [InlineData("release", "https://aka.ms/dotnet/9/aspire/ga/daily/aspire-vscode.vsix.zip")]
    public async Task GetAspireExtensionUrl_NoVersion_ReturnsAkaMsUrl(string quality, string expectedUrl)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Get-AspireExtensionUrl -Quality '{quality}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedUrl, result.Output.Trim());
    }
 
    [Fact]
    public async Task GetAspireExtensionUrl_WithVersion_ReturnsCiDotNetUrl()
    {
        using var env = new TestEnvironment();
        var version = "13.2.0-preview.1.25366.3";
        using var cmd = new ScriptFunctionCommand(
            s_releaseScript,
            $"Get-AspireExtensionUrl -Version '{version}' -Quality 'dev'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var url = result.Output.Trim();
        Assert.Contains("ci.dot.net/public/aspire", url);
        Assert.Contains(version, url);
        Assert.Contains("vsix.zip", url);
    }
 
    #endregion
}