Files
SilverMetal/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs
sysadmin e3b010530c
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 7m31s
fix(kiosk): pivot to Explorer + policy lockdown (WebView2 wizard renders blank as the SL shell)
5th VM e2e: with the kiosk fully working mechanically (SL engages, silent UAC,
app launches fullscreen as the shell), the MAUI/WebView2 wizard STILL renders
blank — WebView2 never initializes when the app is the bare Shell Launcher shell
with no Explorer (the same app rendered fine in the earlier build launched with
Explorer present). Operator decision: pivot.

- autounattend.xml: restore FirstLogonCommands to launch the wizard elevated over
  the normal (Explorer) first-logon session — where WebView2 works.
- Configure-Kiosk.ps1: drop Shell-Launcher-as-shell entirely; keep the lockdown —
  Keyboard Filter (Win/Start/lock/task-switch/Task-Mgr/Alt+F4), DisableTaskMgr /
  LockWorkstation / FastUserSwitch, and silent-elevation UAC. The wizard runs
  fullscreen-topmost over the locked-down Explorer (covers the taskbar).
- RevertKioskAsync: disable the Keyboard Filter rules for the real user (no SL to
  undo); keep escape-policy + secure-UAC restore. Tests updated.

Keeps the diagnostics from #10 (welcome.log) to confirm the wizard renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:52:15 +01:00

102 lines
4.2 KiB
C#

using Moq;
using SilverOS.Welcome.Core.Apply;
public class BootstrapServiceRevertKioskTests
{
private static Mock<IProcessRunner> Ok()
{
var m = new Mock<IProcessRunner>();
m.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ProcessResult(0, "", ""));
return m;
}
private static Mock<IProcessRunner> Fail()
{
var m = new Mock<IProcessRunner>();
m.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.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<string>(s =>
s.Contains("WEKF_PredefinedKey") &&
s.Contains("Enabled=$false")),
It.IsAny<CancellationToken>()), 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<string>(s =>
s.Contains("Remove-ItemProperty") &&
s.Contains("DisableTaskMgr") &&
s.Contains("DisableLockWorkstation") &&
s.Contains("HideFastUserSwitching")),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task ApplyService_calls_revert_kiosk_before_teardown()
{
var order = new List<string>();
var run = new Mock<IProcessRunner>();
run.Setup(r => r.RunAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Callback<string, string, CancellationToken>((_, a, _) =>
{
if (a.Contains("Invoke-Hardening")) order.Add("modules");
})
.ReturnsAsync(new ProcessResult(0, "", ""));
var acct = new Mock<IAccountService>();
acct.Setup(a => a.CreateAccountsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Callback(() => order.Add("accounts"))
.Returns(Task.CompletedTask);
var bl = new Mock<IBitLockerService>();
bl.Setup(b => b.EnableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Callback(() => order.Add("bitlocker"))
.Returns(Task.CompletedTask);
var boot = new Mock<IBootstrapService>();
boot.Setup(b => b.RevertKioskAsync(It.IsAny<CancellationToken>()))
.Callback(() => order.Add("revert-kiosk"))
.Returns(Task.CompletedTask);
boot.Setup(b => b.TearDownAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Callback(() => order.Add("teardown"))
.Returns(Task.CompletedTask);
var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.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");
await sut.RunAsync(req, new Progress<ApplyProgress>(_ => { }));
// revert-kiosk must precede teardown so the sm-bootstrap SID still resolves.
Assert.Equal(new[] { "modules", "accounts", "bitlocker", "revert-kiosk", "teardown" }, order);
}
}