using Moq; using SilverOS.Welcome.Core.Apply; using SilverOS.Welcome.Core.Apps; public class BootstrapServiceRevertKioskTests { private static Mock Ok() { var m = new Mock(); m.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ProcessResult(0, "", "")); return m; } private static Mock Fail() { var m = new Mock(); m.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ProcessResult(1, "", "the operation failed")); return m; } [Fact] public async Task RevertKioskAsync_is_best_effort_and_does_not_throw_on_nonzero_exit() { // Kiosk revert is best-effort (like TearDownAsync): a non-zero exit must NOT // fail the apply — the real user still gets Explorer regardless of WESL state. var ex = await Record.ExceptionAsync(() => new BootstrapService(Fail().Object).RevertKioskAsync()); Assert.Null(ex); } [Fact] public async Task RevertKioskAsync_disables_keyboard_filter_rules() { var run = Ok(); await new BootstrapService(run.Object).RevertKioskAsync(); // First call: disable the Keyboard Filter predefined-key blocks for the real user. run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => s.Contains("WEKF_PredefinedKey") && s.Contains("Enabled=$false")), It.IsAny()), Times.Once); } [Fact] public async Task RevertKioskAsync_reverts_escape_policies() { var run = Ok(); await new BootstrapService(run.Object).RevertKioskAsync(); // Second call: policy revert — must remove the three escape policy values. run.Verify(r => r.RunAsync("powershell.exe", It.Is(s => s.Contains("Remove-ItemProperty") && s.Contains("DisableTaskMgr") && s.Contains("DisableLockWorkstation") && s.Contains("HideFastUserSwitching")), It.IsAny()), Times.Once); } [Fact] public async Task ApplyService_calls_revert_kiosk_before_teardown() { var order = new List(); var run = new Mock(); run.Setup(r => r.RunAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((_, a, _) => { if (a.Contains("Invoke-Hardening")) order.Add("modules"); }) .ReturnsAsync(new ProcessResult(0, "", "")); var acct = new Mock(); acct.Setup(a => a.CreateAccountsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback(() => order.Add("accounts")) .Returns(Task.CompletedTask); var bl = new Mock(); bl.Setup(b => b.EnableAsync(It.IsAny(), It.IsAny())) .Callback(() => order.Add("bitlocker")) .Returns(Task.CompletedTask); var boot = new Mock(); boot.Setup(b => b.RevertKioskAsync(It.IsAny())) .Callback(() => order.Add("revert-kiosk")) .Returns(Task.CompletedTask); boot.Setup(b => b.TearDownAsync(It.IsAny(), It.IsAny())) .Callback(() => order.Add("teardown")) .Returns(Task.CompletedTask); var installer = new Mock(); installer.Setup(i => i.InstallAsync(It.IsAny>(), It.IsAny>(), It.IsAny())) .ReturnsAsync(System.Array.Empty()); var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, installer.Object, "C:\\hard"); var flavour = new SilverOS.Welcome.Core.Flavours.FlavourManifest { Id = "daily-driver", Hardening = new SilverOS.Welcome.Core.Flavours.HardeningSpec { Modules = new[] { "00" } } }; var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap", System.Array.Empty()); await sut.RunAsync(req, new Progress(_ => { })); // revert-kiosk must precede teardown so the sm-bootstrap SID still resolves. Assert.Equal(new[] { "modules", "accounts", "bitlocker", "revert-kiosk", "teardown" }, order); } }