File: Acquisition\InstallSourceDetectorTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Cli.Acquisition;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Acquisition;
 
public class InstallSourceDetectorTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData("script")]
    [InlineData("pr")]
    [InlineData("winget")]
    [InlineData("brew")]
    [InlineData("dotnet-tool")]
    [InlineData("localhive")]
    [InlineData("nix")]
    public void Detect_PrefersKnownSidecarToOtherSignals(string source)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var binaryDirectory = workspace.CreateDirectory(Path.Combine("mise", "installs", "aspire", "13.5.0"));
        File.WriteAllText(Path.Combine(binaryDirectory.FullName, InstallSidecarReader.SidecarFileName), $$"""{"source":"{{source}}"}""");
        var environment = TestEnvironment.CreateWindows(new Dictionary<string, string?>
        {
            [NpmInstallDetection.PackageEnvironmentVariableName] = NpmInstallDetection.ExpectedPackageName
        });
        var detector = CreateDetector(Path.Combine(binaryDirectory.FullName, "aspire.exe"), environment, new TestWindowsRegistryReader(true));
 
        Assert.Equal(source, detector.Detect());
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("{bad-json")]
    [InlineData("""{"source":"future-route-with-private-data"}""")]
    [InlineData("""{"source":42}""")]
    [InlineData("""{"source":""}""")]
    public void Detect_ReturnsUnknownWithoutRecognizedProvenance(string? sidecar)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        if (sidecar is not null)
        {
            File.WriteAllText(Path.Combine(workspace.Path, InstallSidecarReader.SidecarFileName), sidecar);
        }
        var detector = CreateDetector(Path.Combine(workspace.Path, "aspire"), TestEnvironment.CreateLinux());
 
        Assert.Equal("unknown", detector.Detect());
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("aspire")]
    [InlineData("invalid\0path")]
    public void Detect_ReturnsUnknownForUnavailableProcessPath(string? processPath)
    {
        var detector = CreateDetector(processPath, TestEnvironment.CreateLinux());
 
        Assert.Equal("unknown", detector.Detect());
    }
 
    [Fact]
    public void Detect_DoesNotAttributeManagedHostFromInheritedNpmMarker()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
        {
            [NpmInstallDetection.PackageEnvironmentVariableName] = NpmInstallDetection.ExpectedPackageName
        });
        var detector = CreateDetector(Path.Combine(workspace.Path, "dotnet"), environment);
 
        Assert.Equal("unknown", detector.Detect());
    }
 
    [Theory]
    [InlineData("@microsoft/aspire-cli", "npm")]
    [InlineData("@Microsoft/Aspire-Cli", "unknown")]
    [InlineData("aspire-cli", "unknown")]
    [InlineData("", "unknown")]
    public void Detect_RecognizesOnlyCanonicalNpmPackage(string packageName, string expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
        {
            [NpmInstallDetection.PackageEnvironmentVariableName] = packageName
        });
        var detector = CreateDetector(Path.Combine(workspace.Path, "aspire"), environment);
 
        Assert.Equal(expected, detector.Detect());
    }
 
    [Theory]
    [InlineData(true, "winget")]
    [InlineData(false, "unknown")]
    public void Detect_ProbesWingetWithoutWritingSidecar(bool registryClaim, string expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var processPath = Path.Combine(workspace.Path, "aspire.exe");
        var registry = new TestWindowsRegistryReader
        {
            ProbeCallback = path =>
            {
                Assert.Equal(processPath, path);
                return registryClaim;
            }
        };
        var detector = CreateDetector(processPath, TestEnvironment.CreateWindows(), registry);
 
        Assert.Equal(expected, detector.Detect());
        Assert.Empty(Directory.GetFiles(workspace.Path));
    }
 
    [Fact]
    public void Detect_DoesNotProbeRegistryOnUnix()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var registry = new TestWindowsRegistryReader
        {
            ProbeCallback = _ => throw new InvalidOperationException("The registry must not be read on Unix.")
        };
        var detector = CreateDetector(Path.Combine(workspace.Path, "aspire"), TestEnvironment.CreateLinux(), registry);
 
        Assert.Equal("unknown", detector.Detect());
    }
 
    [Fact]
    public void Detect_ReturnsUnknownWhenProbeCannotReadInstallation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var registry = new TestWindowsRegistryReader
        {
            ProbeCallback = _ => throw new UnauthorizedAccessException()
        };
        var detector = CreateDetector(Path.Combine(workspace.Path, "aspire.exe"), TestEnvironment.CreateWindows(), registry);
 
        Assert.Equal("unknown", detector.Detect());
    }
 
    [Theory]
    [InlineData(".local/share/mise/installs/aspire/13.5.0/aspire", "mise")]
    [InlineData("AppData/Local/mise/installs/aspire/13.5.0/bin/aspire", "mise")]
    [InlineData("usr/local/share/mise/installs/aspire/13.5.0/aspire", "mise")]
    [InlineData("mise/installs/aspire/latest/aspire", "mise")]
    [InlineData("mise/installs/github-microsoft-aspire/13.5.0/aspire", "mise")]
    [InlineData("mise/installs/github-microsoft-aspire/13.5.0/bin/aspire", "mise")]
    [InlineData("mise/installs/aspire/13.5.0/tools/aspire", "unknown")]
    [InlineData("mise/installs/another-tool/13.5.0/aspire", "unknown")]
    [InlineData("mise/installs/aspire/aspire", "unknown")]
    [InlineData("mise/installs/aspire-extra/13.5.0/aspire", "unknown")]
    [InlineData("mise/installs/github-other-aspire/13.5.0/aspire", "unknown")]
    [InlineData("not-mise/installs/aspire/13.5.0/aspire", "unknown")]
    [InlineData("mise/downloads/aspire/13.5.0/aspire", "unknown")]
    [InlineData("mise/shims/aspire", "unknown")]
    public void Detect_MatchesMiseInstallationBoundaries(string relativePath, string expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var processPath = Path.Combine(workspace.Path, relativePath.Replace('/', Path.DirectorySeparatorChar));
        var detector = CreateDetector(processPath, TestEnvironment.CreateLinux());
 
        Assert.Equal(expected, detector.Detect());
    }
 
    [Theory]
    [InlineData("MISE_INSTALLS_DIR", "custom", "custom/aspire/13.5.0/aspire", "mise")]
    [InlineData("MISE_SYSTEM_INSTALLS_DIR", "custom", "custom/aspire/13.5.0/aspire", "mise")]
    [InlineData("MISE_DATA_DIR", "custom", "custom/installs/aspire/13.5.0/aspire", "mise")]
    [InlineData("MISE_INSTALLS_DIR", "custom", "custom/github-microsoft-aspire/13.5.0/aspire", "mise")]
    [InlineData("MISE_DATA_DIR", "custom", "custom/downloads/aspire/13.5.0/aspire", "unknown")]
    [InlineData("MISE_INSTALLS_DIR", "custom", "custom-extra/aspire/13.5.0/aspire", "unknown")]
    [InlineData("MISE_DATA_DIR", "custom", "unrelated/aspire", "unknown")]
    [InlineData("MISE_INSTALLS_DIR", "custom", "custom/other/13.5.0/aspire", "unknown")]
    public void Detect_RequiresMatchingPathForMiseOverrides(string variable, string root, string relativePath, string expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
        {
            [variable] = Path.Combine(workspace.Path, root) + Path.DirectorySeparatorChar
        });
        var processPath = Path.Combine(workspace.Path, relativePath.Replace('/', Path.DirectorySeparatorChar));
        var detector = CreateDetector(processPath, environment);
 
        Assert.Equal(expected, detector.Detect());
    }
 
    [Theory]
    [InlineData("MISE_INSTALLS_DIR", true)]
    [InlineData("MISE_INSTALLS_DIR", false)]
    [InlineData("MISE_SYSTEM_INSTALLS_DIR", true)]
    [InlineData("MISE_SYSTEM_INSTALLS_DIR", false)]
    [InlineData("MISE_DATA_DIR", true)]
    [InlineData("MISE_DATA_DIR", false)]
    [SkipOnPlatform(TestPlatforms.Windows | TestPlatforms.Linux, "Firmlink normalization only applies on macOS.")]
    public void Detect_NormalizesMacOSMiseRootAliases(string variable, bool configuredRootUsesPrivatePrefix)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var root = Path.Combine("/tmp", workspace.WorkspaceRoot.Name, "custom");
        var configuredRoot = configuredRootUsesPrivatePrefix ? "/private" + root : root;
        var processRoot = configuredRootUsesPrivatePrefix ? root : "/private" + root;
        var installsRoot = variable == "MISE_DATA_DIR" ? Path.Combine(processRoot, "installs") : processRoot;
        var environment = TestEnvironment.CreateMacOS(new Dictionary<string, string?>
        {
            [variable] = configuredRoot + Path.DirectorySeparatorChar
        });
        var detector = CreateDetector(Path.Combine(installsRoot, "aspire", "13.5.0", "aspire"), environment);
 
        Assert.Equal("mise", detector.Detect());
    }
 
    [Theory]
    [InlineData(true, "mise")]
    [InlineData(false, "unknown")]
    public void Detect_UsesPlatformPathComparisonForMise(bool windows, string expected)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var environment = windows ? TestEnvironment.CreateWindows() : TestEnvironment.CreateLinux();
        var processPath = Path.Combine(workspace.Path, "MISE", "INSTALLS", "ASPIRE", "13.5.0", windows ? "ASPIRE.EXE" : "aspire");
        var detector = CreateDetector(processPath, environment);
 
        Assert.Equal(expected, detector.Detect());
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("{bad-json")]
    [InlineData("""{"source":"unrecognized"}""")]
    public void Detect_UsesNpmMarkerBeforeMisePathWhenSidecarDoesNotIdentifySource(string? sidecar)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var directory = workspace.CreateDirectory(Path.Combine("mise", "installs", "aspire", "13.5.0"));
        if (sidecar is not null)
        {
            File.WriteAllText(Path.Combine(directory.FullName, InstallSidecarReader.SidecarFileName), sidecar);
        }
        var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
        {
            [NpmInstallDetection.PackageEnvironmentVariableName] = NpmInstallDetection.ExpectedPackageName
        });
        var detector = CreateDetector(Path.Combine(directory.FullName, "aspire"), environment);
 
        Assert.Equal("npm", detector.Detect());
    }
 
    [Theory]
    [InlineData(".dotnet/tools/aspire")]
    [InlineData("custom/.store/aspire.cli/13.5.0/aspire.cli.linux-x64/13.5.0/tools/any/linux-x64/aspire")]
    public void Detect_RecognizesDotNetToolsWithoutSidecar(string relativePath)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var processPath = Path.Combine(workspace.Path, relativePath.Replace('/', Path.DirectorySeparatorChar));
        var detector = CreateDetector(processPath, TestEnvironment.CreateLinux());
 
        Assert.Equal("dotnet-tool", detector.Detect());
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "Symlink creation requires additional privileges on Windows.")]
    public void Detect_ReadsSidecarBesideResolvedExecutable()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var installDirectory = workspace.CreateDirectory("installed");
        var processPath = Path.Combine(installDirectory.FullName, "aspire");
        File.WriteAllText(processPath, string.Empty);
        File.WriteAllText(Path.Combine(installDirectory.FullName, InstallSidecarReader.SidecarFileName), """{"source":"brew"}""");
        var linkPath = Path.Combine(workspace.Path, "aspire");
        File.CreateSymbolicLink(linkPath, processPath);
        var detector = CreateDetector(linkPath, TestEnvironment.CreateLinux());
 
        Assert.Equal("brew", detector.Detect());
    }
 
    private InstallSourceDetector CreateDetector(string? processPath, IEnvironment environment, IWindowsRegistryReader? registry = null)
    {
        return new InstallSourceDetector(
            new TestProcessPathProvider(processPath),
            CliTestHelper.CreateSidecarReader(outputHelper),
            registry ?? new TestWindowsRegistryReader(),
            environment,
            NullLogger<InstallSourceDetector>.Instance);
    }
}