File: WindowsTrayStartupSettingsTests.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.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.Runtime.Versioning;
using Aspire.Tray.Tests.Helpers;
using Microsoft.Win32;
 
namespace Aspire.Tray.Tests;
 
public class WindowsTrayStartupSettingsTests
{
    public static bool SupportsWindows => OperatingSystem.IsWindows();
 
    [Fact(Skip = "Registry behavior requires Windows; this test never uses the real Run key.", SkipUnless = nameof(SupportsWindows))]
    [SupportedOSPlatform("windows")]
    public void RegistryStoreTouchesOnlyItsValueInAnIsolatedKey()
    {
        var path = $@"Software\Aspire.Tray.Tests\Startup-{Guid.NewGuid():N}";
        var store = new WindowsRunStartupRegistrationStore(Registry.CurrentUser, path, "AspireTray");
        try
        {
            Assert.Null(store.Read());
            using var absent = Registry.CurrentUser.OpenSubKey(path);
            Assert.Null(absent);
            store.Write(null, "owned");
            using (var key = Registry.CurrentUser.OpenSubKey(path, writable: true))
            {
                Assert.NotNull(key);
                key.SetValue("OtherApplication", "unchanged");
                key.SetValue("AspireTray", "external");
            }
            Assert.Throws<InvalidOperationException>(() => store.Write("owned", null));
            Assert.Equal("external", store.Read());
            store.Write("external", null);
            using var remaining = Registry.CurrentUser.OpenSubKey(path);
            Assert.NotNull(remaining);
            Assert.Equal("unchanged", remaining.GetValue("OtherApplication"));
        }
        finally
        {
            Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
        }
    }
 
    [Fact]
    public void ReadingOrDisablingDoesNotInstallAnything()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
 
        Assert.False(settings.Read().Enabled);
        Assert.False(settings.SetEnabled(false).Enabled);
        Assert.Equal(0, store.WriteCount);
        Assert.False(Directory.Exists(Path.GetDirectoryName(installation.Bootstrap)));
    }
 
    [Fact]
    public void EnableCopiesTheGuiAndRegistersOnlyTheBootstrap()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
 
        Assert.True(settings.SetEnabled(true).Enabled);
        Assert.Equal(WindowsTrayStartupSettings.BuildRunCommand(installation.Bootstrap, installation.WindowsCli), store.Value);
        Assert.Equal(File.ReadAllBytes(installation.SourceGui), File.ReadAllBytes(installation.Bootstrap));
        var timestamp = File.GetLastWriteTimeUtc(installation.Bootstrap);
        Assert.True(settings.SetEnabled(true).Enabled);
        Assert.Equal(1, store.WriteCount);
        Assert.Equal(timestamp, File.GetLastWriteTimeUtc(installation.Bootstrap));
 
        var reopened = installation.WindowsSettings(store, nativeFrontend: true);
        Assert.True(reopened.Read().Enabled);
        Assert.False(reopened.SetEnabled(false).Enabled);
        Assert.Null(store.Value);
        Assert.True(File.Exists(installation.Bootstrap));
    }
 
    [Fact]
    public void DisableDoesNotRequireTheOriginalCliOrNativeDevelopmentHost()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        installation.WindowsSettings(store, nativeFrontend: true).SetEnabled(true);
        File.Delete(installation.WindowsCli);
        var settings = installation.WindowsSettings(store, nativeFrontend: false);
 
        Assert.True(settings.Read().Enabled);
        Assert.False(settings.Read().CanEnable);
        Assert.False(settings.SetEnabled(false).Enabled);
        Assert.Null(store.Value);
    }
 
    [Theory]
    [InlineData("foreign.exe --run")]
    [InlineData("")]
    public void UnrelatedRunValueIsNeverChanged(string registration)
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore { Value = registration };
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
 
        Assert.Throws<InvalidOperationException>(settings.Read);
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(false));
        Assert.Equal(registration, store.Value);
        Assert.Equal(0, store.WriteCount);
    }
 
    [Fact]
    public void ExternalEditBetweenReadAndToggleIsNotOverwritten()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
        settings.Read();
        store.Value = WindowsTrayStartupSettings.BuildRunCommand(installation.Bootstrap, Path.Combine(installation.Root, "different.exe"));
 
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.Equal(0, store.WriteCount);
    }
 
    [Fact]
    public void StoreRechecksAnExternalEditImmediatelyBeforeWrite()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
        settings.Read();
        store.BeforeWrite = () => store.Value = "external";
 
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.Equal("external", store.Value);
        Assert.Equal(0, store.WriteCount);
    }
 
    [Fact]
    public void UnownedOrModifiedBootstrapIsPreserved()
    {
        using var installation = new TestTrayStartupInstallation();
        Directory.CreateDirectory(Path.GetDirectoryName(installation.Bootstrap)!);
        File.Copy(installation.SourceGui, installation.Bootstrap);
        var original = File.ReadAllBytes(installation.Bootstrap);
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
 
        Assert.False(settings.Read().CanEnable);
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.Equal(original, File.ReadAllBytes(installation.Bootstrap));
        Assert.Null(store.Value);
    }
 
    [Fact]
    public void ManagedGuiCannotBeCopiedAsTheBootstrap()
    {
        using var installation = new TestTrayStartupInstallation();
        File.WriteAllText(Path.ChangeExtension(installation.SourceGui, ".runtimeconfig.json"), "{}");
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
 
        Assert.False(settings.Read().CanEnable);
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.False(File.Exists(installation.Bootstrap));
        Assert.Null(store.Value);
    }
 
    [Fact]
    public void EnvironmentVariableDelimitersCannotChangeTheRegisteredPath()
    {
        using var installation = new TestTrayStartupInstallation();
        var cli = Path.Combine(installation.Root, "%INSTALL%.exe");
        File.Copy(installation.WindowsCli, cli);
        var options = installation.Options(windows: true) with { CliPath = cli, StartupCliPath = cli };
        var store = new TestTrayStartupRegistrationStore();
        var settings = new WindowsTrayStartupSettings(options, true, installation.SourceGui, installation.Bootstrap, store);
 
        Assert.False(settings.Read().CanEnable);
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.Equal(0, store.WriteCount);
    }
 
    [Fact]
    public void BootstrapRemainsStableAcrossBackendUpgradesAndCanBeDisabledAfterExternalChanges()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        var settings = installation.WindowsSettings(store, nativeFrontend: true);
        settings.SetEnabled(true);
        var original = File.ReadAllBytes(installation.Bootstrap);
        using (var source = new FileStream(installation.SourceGui, FileMode.Append, FileAccess.Write))
        {
            source.WriteByte(1);
        }
        settings.SetEnabled(false);
        settings.SetEnabled(true);
        Assert.Equal(original, File.ReadAllBytes(installation.Bootstrap));
 
        File.AppendAllText(installation.Bootstrap, "external edit");
        Assert.False(settings.Read().CanEnable);
        Assert.False(settings.SetEnabled(false).Enabled);
        Assert.Null(store.Value);
    }
 
    [Fact]
    public void OverlongRunCommandCannotBeEnabled()
    {
        using var installation = new TestTrayStartupInstallation();
        var store = new TestTrayStartupRegistrationStore();
        // The root is much shorter on Linux runners than on macOS. Make the command
        // exceed the Run key limit independently of the machine's temporary path.
        var bootstrap = Path.Combine(installation.Root, new string('a', 150), new string('b', 150), "aspire-tray-login.exe");
        var settings = new WindowsTrayStartupSettings(installation.Options(windows: true), true, installation.SourceGui, bootstrap, store);
 
        Assert.False(settings.Read().CanEnable);
        Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
        Assert.Equal(0, store.WriteCount);
    }
 
    [Theory]
    [InlineData(260, true)]
    [InlineData(261, false)]
    public void RunCommandLengthLimitIsExact(int length, bool canEnable)
    {
        using var installation = new TestTrayStartupInstallation();
        var originalLength = WindowsTrayStartupSettings.BuildRunCommand(installation.Bootstrap, installation.WindowsCli).Length;
        Assert.True(originalLength < 260);
        var cli = Path.Combine(installation.Root, new string('a', length - originalLength) + Path.GetFileName(installation.WindowsCli));
        File.Copy(installation.WindowsCli, cli);
        var options = installation.Options(windows: true) with { CliPath = cli, StartupCliPath = cli };
        var store = new TestTrayStartupRegistrationStore();
        var settings = new WindowsTrayStartupSettings(options, true, installation.SourceGui, installation.Bootstrap, store);
 
        Assert.Equal(length, WindowsTrayStartupSettings.BuildRunCommand(installation.Bootstrap, cli).Length);
        Assert.Equal(canEnable, settings.Read().CanEnable);
        if (canEnable)
        {
            Assert.True(settings.SetEnabled(true).Enabled);
            Assert.Equal(length, store.Value!.Length);
        }
        else
        {
            Assert.Throws<InvalidOperationException>(() => settings.SetEnabled(true));
            Assert.Equal(0, store.WriteCount);
        }
    }
 
    [Fact]
    public void BootstrapUsesOnlyExplicitTrayStartArgumentsWithoutAConsole()
    {
        using var installation = new TestTrayStartupInstallation();
        var start = WindowsLoginBootstrap.CreateStartInfo(installation.WindowsCli);
 
        Assert.Equal(installation.WindowsCli, start.FileName);
        Assert.False(start.UseShellExecute);
        Assert.True(start.CreateNoWindow);
        Assert.True(start.RedirectStandardInput);
        Assert.True(start.RedirectStandardOutput);
        Assert.True(start.RedirectStandardError);
        Assert.Equal(["tray", "start", "--non-interactive", "--nologo"], start.ArgumentList);
    }
}