diff --git a/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs b/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs index c9225e3..322c36f 100644 --- a/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs +++ b/windows/welcome/src/SilverOS.Welcome.App/MauiProgram.cs @@ -39,11 +39,14 @@ public static class MauiProgram builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + var appsDir = Path.Combine(AppContext.BaseDirectory, "apps"); + builder.Services.AddSingleton(sp => new AppInstaller(sp.GetRequiredService(), appsDir)); builder.Services.AddSingleton(sp => new ApplyService( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), + sp.GetRequiredService(), hardeningDir)); builder.Services.AddScoped(); diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyRequest.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyRequest.cs index a6b0cd9..a1c8179 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyRequest.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyRequest.cs @@ -1,4 +1,6 @@ +using SilverOS.Welcome.Core.Apps; using SilverOS.Welcome.Core.Flavours; namespace SilverOS.Welcome.Core.Apply; public sealed record ApplyRequest(FlavourManifest Flavour, string Username, string Password, - string AdminPassword, string BitLockerPin, string BootstrapUser); + string AdminPassword, string BitLockerPin, string BootstrapUser, + IReadOnlyList Apps); diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyService.cs b/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyService.cs index 8e1fa3f..17727d2 100644 --- a/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyService.cs +++ b/windows/welcome/src/SilverOS.Welcome.Core/Apply/ApplyService.cs @@ -1,9 +1,11 @@ using System.Text.Json; +using SilverOS.Welcome.Core.Apps; using SilverOS.Welcome.Core.Flavours; namespace SilverOS.Welcome.Core.Apply; public sealed class ApplyService(IProcessRunner runner, IAccountService accounts, - IBitLockerService bitlocker, IBootstrapService bootstrap, string hardeningDir) : IApplyService + IBitLockerService bitlocker, IBootstrapService bootstrap, IAppInstaller installer, + string hardeningDir) : IApplyService { public async Task RunAsync(ApplyRequest req, IProgress progress, CancellationToken ct = default) { @@ -29,6 +31,9 @@ public sealed class ApplyService(IProcessRunner runner, IAccountService accounts progress.Report(new("Creating your account", 55)); await accounts.CreateAccountsAsync(req.Username, req.Password, req.AdminPassword, ct); + progress.Report(new("Installing apps", 70)); + await installer.InstallAsync(req.Apps, progress, ct); // continue-on-failure; never throws + progress.Report(new("Encrypting the disk", 75)); await bitlocker.EnableAsync(req.BitLockerPin, ct); diff --git a/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor b/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor index 099cb73..797a1e2 100644 --- a/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor +++ b/windows/welcome/src/SilverOS.Welcome.UI/Components/Steps/ApplyStep.razor @@ -1,4 +1,6 @@ +@using SilverOS.Welcome.Core.Apps @inject IApplyService ApplyService +@inject IAppCatalog AppCatalog @inject WizardState State
@@ -81,13 +83,17 @@ StateHasChanged(); await OnRunningChanged.InvokeAsync(true); + var apps = AppCatalog.Load(Path.Combine(AppContext.BaseDirectory, "apps")) + .All.Where(a => State.SelectedApps.Contains(a.Id)).ToList(); + var req = new ApplyRequest( Flavour: State.Flavour!, Username: State.Username, Password: State.Password, AdminPassword: State.AdminPassword, BitLockerPin: State.BitLockerPin, - BootstrapUser: "sm-bootstrap"); + BootstrapUser: "sm-bootstrap", + Apps: apps); var progress = new Progress(p => { diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceHardeningIntegrationTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceHardeningIntegrationTests.cs index e63bbd7..f8a5652 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceHardeningIntegrationTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceHardeningIntegrationTests.cs @@ -1,5 +1,6 @@ using Moq; using SilverOS.Welcome.Core.Apply; +using SilverOS.Welcome.Core.Apps; using SilverOS.Welcome.Core.Flavours; using Xunit; @@ -69,11 +70,18 @@ public class ApplyServiceHardeningIntegrationTests boot.Setup(b => b.TearDownAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); + var installer = new Mock(); + installer.Setup(i => i.InstallAsync( + It.IsAny>(), + It.IsAny>(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + var sut = new ApplyService( runner: new ProcessRunner(), accounts: acct.Object, bitlocker: bl.Object, bootstrap: boot.Object, + installer: installer.Object, hardeningDir: tmp); // Flavour requests modules 00 and 05 only — 03 and 07 must be skipped. @@ -82,7 +90,7 @@ public class ApplyServiceHardeningIntegrationTests Id = "test", Hardening = new HardeningSpec { Modules = new[] { "00", "05" } } }; - var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap"); + var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap", Array.Empty()); // ---- Act ---- await sut.RunAsync(req, new Progress(_ => { })); diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceTests.cs index ea26127..5c81adf 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyServiceTests.cs @@ -1,10 +1,20 @@ using Moq; using SilverOS.Welcome.Core.Apply; +using SilverOS.Welcome.Core.Apps; using SilverOS.Welcome.Core.Flavours; using Xunit; public class ApplyServiceTests { + private static Mock NoApps() + { + var installer = new Mock(); + installer.Setup(i => i.InstallAsync(It.IsAny>(), + It.IsAny>(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + return installer; + } + [Fact] public async Task Runs_modules_then_accounts_then_bitlocker_then_bootstrap_last() { @@ -16,15 +26,16 @@ public class ApplyServiceTests 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.TearDownAsync(It.IsAny(),It.IsAny())).Callback(() => order.Add("bootstrap")).Returns(Task.CompletedTask); + var installer = NoApps(); installer.Setup(i => i.InstallAsync(It.IsAny>(),It.IsAny>(),It.IsAny())).Callback(() => order.Add("apps")).ReturnsAsync(Array.Empty()); - var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, "C:\\hard"); + var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, installer.Object, "C:\\hard"); var flavour = new FlavourManifest { Id="daily-driver", Hardening = new HardeningSpec { Modules = new[]{"00"} } }; - var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap"); + var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap", System.Array.Empty()); var progress = new List(); await sut.RunAsync(req, new Progress(p => progress.Add(p.Stage))); - Assert.Equal(new[]{"modules","accounts","bitlocker","bootstrap"}, order); + Assert.Equal(new[]{"modules","accounts","apps","bitlocker","bootstrap"}, order); Assert.Contains("Applying hardening", progress); } @@ -34,8 +45,8 @@ public class ApplyServiceTests var run = new Mock(); run.Setup(r => r.RunAsync(It.IsAny(),It.IsAny(),It.IsAny())).ReturnsAsync(new ProcessResult(0,"","")); var acct = new Mock(); acct.Setup(a => a.CreateAccountsAsync(It.IsAny(),It.IsAny(),It.IsAny(),It.IsAny())).ThrowsAsync(new InvalidOperationException("boom")); var bl = new Mock(); var boot = new Mock(); - var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, "C:\\hard"); - var req = new ApplyRequest(new FlavourManifest{ Hardening=new HardeningSpec{Modules=new[]{"00"}}}, "a","b","c","123456","sm-bootstrap"); + var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, NoApps().Object, "C:\\hard"); + var req = new ApplyRequest(new FlavourManifest{ Hardening=new HardeningSpec{Modules=new[]{"00"}}}, "a","b","c","123456","sm-bootstrap", System.Array.Empty()); await Assert.ThrowsAsync(() => sut.RunAsync(req, new Progress(_ => {}))); boot.Verify(b => b.TearDownAsync(It.IsAny(), It.IsAny()), Times.Never); } diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs index e40a9d8..45429d9 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/ApplyStepTests.cs @@ -5,11 +5,18 @@ using Microsoft.Extensions.DependencyInjection; using SilverOS.Welcome.App.Components; using SilverOS.Welcome.App.Components.Steps; using SilverOS.Welcome.Core.Apply; +using SilverOS.Welcome.Core.Apps; using SilverOS.Welcome.Core.Flavours; using Xunit; public class ApplyStepTests : TestContext { + // The component loads the catalog from AppContext.BaseDirectory/apps; no catalog.json + // is staged in the test bin, so the real AppCatalog degrades to an empty list — which is + // exactly what these tests want (no apps selected → empty Apps on the request). + private static void AddCatalog(IServiceCollection services) => + services.AddSingleton(new AppCatalog()); + [Fact] public async Task Calls_apply_with_the_wizard_selections() { @@ -24,6 +31,7 @@ public class ApplyStepTests : TestContext }; Services.AddSingleton(state); Services.AddSingleton(apply.Object); + AddCatalog(Services); var cut = RenderComponent(); await cut.InvokeAsync(() => cut.Instance.StartAsync()); apply.Verify(a => a.RunAsync( @@ -45,6 +53,7 @@ public class ApplyStepTests : TestContext }; Services.AddSingleton(state); Services.AddSingleton(apply.Object); + AddCatalog(Services); var completed = false; var cut = RenderComponent(p => p.Add(s => s.OnComplete, EventCallback.Factory.Create(this, () => { completed = true; }))); await cut.InvokeAsync(() => cut.Instance.StartAsync()); @@ -64,6 +73,7 @@ public class ApplyStepTests : TestContext }; Services.AddSingleton(state); Services.AddSingleton(apply.Object); + AddCatalog(Services); var cut = RenderComponent(); await cut.InvokeAsync(() => cut.Instance.StartAsync()); Assert.Contains("Module 03 failed", cut.Markup); diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs index 7c9b5e7..e96b24b 100644 --- a/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/BootstrapServiceRevertKioskTests.cs @@ -1,5 +1,6 @@ using Moq; using SilverOS.Welcome.Core.Apply; +using SilverOS.Welcome.Core.Apps; public class BootstrapServiceRevertKioskTests { @@ -85,13 +86,18 @@ public class BootstrapServiceRevertKioskTests .Callback(() => order.Add("teardown")) .Returns(Task.CompletedTask); - var sut = new ApplyService(run.Object, acct.Object, bl.Object, boot.Object, "C:\\hard"); + 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"); + var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap", System.Array.Empty()); await sut.RunAsync(req, new Progress(_ => { }));