File: Scripts\PRScriptFunctionTests.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 Microsoft.DotNet.XUnitExtensions;
using Xunit;
 
namespace Aspire.Acquisition.Tests.Scripts;
 
/// <summary>
/// Unit tests for individual functions in the bash PR script (get-aspire-cli-pr.sh).
/// Tests RID computation and version suffix extraction.
/// </summary>
[SkipOnPlatform(TestPlatforms.Windows, "Bash script tests require bash shell")]
public class PRScriptFunctionTests(ITestOutputHelper testOutput)
{
    private static readonly string s_prScript = ScriptPaths.PRShell;
 
    private readonly ITestOutputHelper _testOutput = testOutput;
 
    #region get_runtime_identifier
 
    [Theory]
    [InlineData("linux", "x64", "linux-x64")]
    [InlineData("linux", "arm64", "linux-arm64")]
    [InlineData("osx", "arm64", "osx-arm64")]
    [InlineData("osx", "x64", "osx-x64")]
    [InlineData("win", "x64", "win-x64")]
    [InlineData("win", "arm64", "win-arm64")]
    public async Task GetRuntimeIdentifier_ExplicitOsAndArch_ReturnsExpectedRid(
        string os, string arch, string expectedRid)
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"get_runtime_identifier '{os}' '{arch}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expectedRid, result.Output.Trim());
    }
 
    [Fact]
    public async Task GetRuntimeIdentifier_UnsupportedArch_Fails()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            "get_runtime_identifier 'linux' 'mips'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("not supported", result.Output, StringComparison.OrdinalIgnoreCase);
    }
 
    [Theory]
    [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_prScript,
            $"get_cli_architecture_from_architecture '{input}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal(expected, result.Output.Trim());
    }
 
    #endregion
 
    #region extract_version_suffix_from_packages
 
    [Fact]
    public async Task ExtractVersionSuffix_ValidNupkg_ReturnsVersionSuffix()
    {
        using var env = new TestEnvironment();
 
        var pkgDir = Path.Combine(env.TempDirectory, "packages");
        await FakeArchiveHelper.CreateFakeNupkgAsync(
            pkgDir,
            "Aspire.Hosting",
            "13.2.0-pr.12345.a1b2c3d4");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"extract_version_suffix_from_packages '{pkgDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal("pr.12345.a1b2c3d4", result.Output.Trim());
    }
 
    [Fact]
    public async Task ExtractVersionSuffix_NoNupkgFiles_Fails()
    {
        using var env = new TestEnvironment();
 
        var emptyDir = Path.Combine(env.TempDirectory, "empty-packages");
        Directory.CreateDirectory(emptyDir);
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"extract_version_suffix_from_packages '{emptyDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
    }
 
    [Fact]
    public async Task ExtractVersionSuffix_NupkgWithoutPrSuffix_Fails()
    {
        using var env = new TestEnvironment();
 
        var pkgDir = Path.Combine(env.TempDirectory, "packages");
        await FakeArchiveHelper.CreateFakeNupkgAsync(
            pkgDir,
            "Aspire.Hosting",
            "13.2.0-release");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"extract_version_suffix_from_packages '{pkgDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
    }
 
    [Fact]
    public async Task ExtractVersionSuffix_MultipleNupkgs_UsesFirst()
    {
        using var env = new TestEnvironment();
 
        var pkgDir = Path.Combine(env.TempDirectory, "packages");
        await FakeArchiveHelper.CreateFakeNupkgAsync(
            pkgDir,
            "Aspire.Hosting",
            "13.2.0-pr.99999.deadbeef");
        await FakeArchiveHelper.CreateFakeNupkgAsync(
            pkgDir,
            "Aspire.Dashboard",
            "13.2.0-pr.99999.deadbeef");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"extract_version_suffix_from_packages '{pkgDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Equal("pr.99999.deadbeef", result.Output.Trim());
    }
 
    #endregion
 
    #region detect_os (PR script copy)
 
    [Fact]
    public async Task DetectOs_ReturnsKnownPlatform()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            "detect_os",
            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 detect_architecture (PR script copy)
 
    [Fact]
    public async Task DetectArchitecture_ReturnsKnownArch()
    {
        using var env = new TestEnvironment();
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            "detect_architecture",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var arch = result.Output.Trim();
        Assert.True(
            arch is "x64" or "arm64",
            $"Expected x64 or arm64, got: '{arch}'");
    }
 
    #endregion
 
    #region remove_temp_dir
 
    [Fact]
    public async Task RemoveTempDir_KeepArchive_RetainsDirectory()
    {
        using var env = new TestEnvironment();
        var tempDir = Path.Combine(env.TempDirectory, "download-temp");
        Directory.CreateDirectory(tempDir);
        File.WriteAllText(Path.Combine(tempDir, "artifact.tar.gz"), "fake");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"KEEP_ARCHIVE=true; DRY_RUN=false; VERBOSE=true; remove_temp_dir '{tempDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.True(Directory.Exists(tempDir), "Directory should be retained when KEEP_ARCHIVE=true");
    }
 
    [Fact]
    public async Task RemoveTempDir_NoKeepArchive_DeletesDirectory()
    {
        using var env = new TestEnvironment();
        var tempDir = Path.Combine(env.TempDirectory, "download-temp");
        Directory.CreateDirectory(tempDir);
        File.WriteAllText(Path.Combine(tempDir, "artifact.tar.gz"), "fake");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"KEEP_ARCHIVE=false; DRY_RUN=false; VERBOSE=true; remove_temp_dir '{tempDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.False(Directory.Exists(tempDir), "Directory should be deleted when KEEP_ARCHIVE=false");
    }
 
    [Fact]
    public async Task RemoveTempDir_DryRun_RetainsDirectory()
    {
        using var env = new TestEnvironment();
        var tempDir = Path.Combine(env.TempDirectory, "download-temp");
        Directory.CreateDirectory(tempDir);
        File.WriteAllText(Path.Combine(tempDir, "artifact.tar.gz"), "fake");
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"KEEP_ARCHIVE=false; DRY_RUN=true; VERBOSE=true; remove_temp_dir '{tempDir}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.True(Directory.Exists(tempDir), "Directory should be retained during dry run");
    }
 
    #endregion
 
    #region download_aspire_cli archive selection
 
    [Fact]
    public async Task DownloadAspireCli_ZeroArchives_Fails()
    {
        using var env = new TestEnvironment();
        var mockBinDir = await env.CreateMockGhScriptAsync(_testOutput);
        var tempDir = Path.Combine(env.TempDirectory, "work");
        Directory.CreateDirectory(tempDir);
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"VERBOSE=true; DRY_RUN=false; download_aspire_cli '987654321' 'linux-x64' '{tempDir}'",
            env,
            _testOutput);
        cmd.WithEnvironmentVariable("PATH", $"{mockBinDir}:{Environment.GetEnvironmentVariable("PATH")}");
        cmd.WithEnvironmentVariable("ASPIRE_REPO", "dotnet/aspire");
        // Mock gh creates a non-matching file so the archive search finds nothing
        cmd.WithEnvironmentVariable("MOCK_GH_DOWNLOAD_FILES", "README.txt");
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("No CLI archive found", result.Output);
    }
 
    [Fact]
    public async Task DownloadAspireCli_MultipleArchives_Fails()
    {
        using var env = new TestEnvironment();
        var mockBinDir = await env.CreateMockGhScriptAsync(_testOutput);
        var tempDir = Path.Combine(env.TempDirectory, "work");
        Directory.CreateDirectory(tempDir);
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"VERBOSE=true; DRY_RUN=false; download_aspire_cli '987654321' 'linux-x64' '{tempDir}'",
            env,
            _testOutput);
        cmd.WithEnvironmentVariable("PATH", $"{mockBinDir}:{Environment.GetEnvironmentVariable("PATH")}");
        cmd.WithEnvironmentVariable("ASPIRE_REPO", "dotnet/aspire");
        cmd.WithEnvironmentVariable("MOCK_GH_DOWNLOAD_FILES", "aspire-cli-linux-x64.tar.gz\naspire-cli-osx-arm64.tar.gz");
 
        var result = await cmd.ExecuteAsync();
 
        Assert.NotEqual(0, result.ExitCode);
        Assert.Contains("Multiple CLI archives found", result.Output);
    }
 
    [Fact]
    public async Task DownloadAspireCli_SingleArchive_ReturnsPath()
    {
        using var env = new TestEnvironment();
        var mockBinDir = await env.CreateMockGhScriptAsync(_testOutput);
        var tempDir = Path.Combine(env.TempDirectory, "work");
        Directory.CreateDirectory(tempDir);
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"VERBOSE=true; DRY_RUN=false; download_aspire_cli '987654321' 'linux-x64' '{tempDir}'",
            env,
            _testOutput);
        cmd.WithEnvironmentVariable("PATH", $"{mockBinDir}:{Environment.GetEnvironmentVariable("PATH")}");
        cmd.WithEnvironmentVariable("ASPIRE_REPO", "dotnet/aspire");
        cmd.WithEnvironmentVariable("MOCK_GH_DOWNLOAD_FILES", "aspire-cli-linux-x64.tar.gz");
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        Assert.Contains("aspire-cli-linux-x64.tar.gz", result.Output);
    }
 
    #endregion
 
    #region add_to_path PR install
 
    [Fact]
    public async Task AddToPath_AppendsExportLine()
    {
        // add_to_path is the lower-level helper invoked by add_to_shell_profile when the
        // shell config is writable. PR installs deliberately bypass add_to_shell_profile
        // entirely (verified by PR-install end-to-end behavior); this test pins the
        // remaining contract: when called directly with a writable config, it appends a
        // single `export PATH=...` line preceded by the marker comment.
        using var env = new TestEnvironment();
        var configFile = Path.Combine(env.MockHome, ".zshrc");
        File.WriteAllText(configFile, "# existing config\n");
 
        var newPath = Path.Combine(env.MockHome, ".aspire", "bin");
        var newCommand = "export PATH=\"$HOME/.aspire/bin:$PATH\"";
 
        using var cmd = new ScriptFunctionCommand(
            s_prScript,
            $"VERBOSE=true; DRY_RUN=false; add_to_path '{configFile}' '{newPath}' '{newCommand}'",
            env,
            _testOutput);
 
        var result = await cmd.ExecuteAsync();
 
        result.EnsureSuccessful();
        var contents = File.ReadAllText(configFile);
        Assert.Contains("# existing config", contents);
        Assert.Contains("# Added by get-aspire-cli*.sh script", contents);
        Assert.Contains("$HOME/.aspire/bin", contents);
    }
 
    #endregion
}