using CursorLang.Core.Models; using CursorLang.Core.Services; using CursorLang.Tests.Shared; using Microsoft.Win32; namespace CursorLang.Core.Tests.Services; /// /// Startup of a build unpacked into a folder: a value under the Run key. The tests /// keep to a root of their own, so the startup list of the machine is untouched. /// public sealed class RegistryStartupTests { private const string RunPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; private const string ApprovedPath = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"; private const string ValueName = "CursorLang"; private const string Command = @"""C:\Apps\CursorLang\CursorLang.exe"""; [Fact] public void With_nothing_written_down_startup_is_off() { using var root = new TempRegistryKey(); Assert.Equal(StartupState.Disabled, new RegistryStartup(root.Key, Command).GetState()); } [Fact] public void Switching_startup_on_writes_the_path_of_the_app() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); Assert.Equal(StartupState.Enabled, startup.SetEnabled(true)); Assert.Equal(Command, ReadRunValue(root)); } [Fact] public void Switching_startup_off_takes_the_entry_away() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); startup.SetEnabled(true); Assert.Equal(StartupState.Disabled, startup.SetEnabled(false)); Assert.Null(ReadRunValue(root)); } // Switching off what is already off is what happens when Windows and the app // disagree about the state, and it is no reason to fail [Fact] public void Switching_off_startup_that_is_already_off_passes_quietly() { using var root = new TempRegistryKey(); Assert.Equal(StartupState.Disabled, new RegistryStartup(root.Key, Command).SetEnabled(false)); } [Fact] public void The_path_is_written_afresh_every_time() { using var root = new TempRegistryKey(); new RegistryStartup(root.Key, @"""C:\Old\CursorLang.exe""").SetEnabled(true); new RegistryStartup(root.Key, Command).SetEnabled(true); Assert.Equal(Command, ReadRunValue(root)); } [Fact] public void An_entry_the_user_has_banned_counts_as_off() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); startup.SetEnabled(true); Ban(root); Assert.Equal(StartupState.DisabledByUser, startup.GetState()); } // The ban outlives the request: the entry is written, and Windows still ignores it [Fact] public void The_ban_of_the_user_survives_a_request_to_switch_startup_on() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); Ban(root); Assert.Equal(StartupState.DisabledByUser, startup.SetEnabled(true)); Assert.Equal(Command, ReadRunValue(root)); } [Fact] public void A_verdict_of_the_user_in_favour_leaves_startup_on() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); startup.SetEnabled(true); WriteVerdict(root, 0x02); Assert.Equal(StartupState.Enabled, startup.GetState()); } // An empty blob is not a ban: Windows writes twelve bytes, but a value cut // short says nothing about the will of the user [Fact] public void A_verdict_with_no_bytes_in_it_is_no_ban() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, Command); startup.SetEnabled(true); using RegistryKey approved = root.Key.CreateSubKey(ApprovedPath); approved.SetValue(ValueName, Array.Empty(), RegistryValueKind.Binary); Assert.Equal(StartupState.Enabled, startup.GetState()); } [Fact] public void With_no_path_to_the_app_startup_is_unavailable() { using var root = new TempRegistryKey(); var startup = new RegistryStartup(root.Key, command: null); Assert.Equal(StartupState.Unavailable, startup.GetState()); Assert.Equal(StartupState.Unavailable, startup.SetEnabled(true)); Assert.Equal(StartupState.Unavailable, startup.SetEnabled(false)); Assert.Null(ReadRunValue(root)); } private static string? ReadRunValue(TempRegistryKey root) { using RegistryKey? run = root.Key.OpenSubKey(RunPath); return run?.GetValue(ValueName) as string; } /// The mark Windows leaves after the user switches the entry off. private static void Ban(TempRegistryKey root) => WriteVerdict(root, 0x03); private static void WriteVerdict(TempRegistryKey root, byte first) { var verdict = new byte[12]; verdict[0] = first; using RegistryKey approved = root.Key.CreateSubKey(ApprovedPath); approved.SetValue(ValueName, verdict, RegistryValueKind.Binary); } }