feat(apps): install selected apps during Apply (after accounts, before BitLocker)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,14 @@ public static class MauiProgram
|
||||
builder.Services.AddSingleton<IAccountService, AccountService>();
|
||||
builder.Services.AddSingleton<IBitLockerService, BitLockerService>();
|
||||
builder.Services.AddSingleton<IBootstrapService, BootstrapService>();
|
||||
var appsDir = Path.Combine(AppContext.BaseDirectory, "apps");
|
||||
builder.Services.AddSingleton<IAppInstaller>(sp => new AppInstaller(sp.GetRequiredService<IProcessRunner>(), appsDir));
|
||||
builder.Services.AddSingleton<IApplyService>(sp => new ApplyService(
|
||||
sp.GetRequiredService<IProcessRunner>(),
|
||||
sp.GetRequiredService<IAccountService>(),
|
||||
sp.GetRequiredService<IBitLockerService>(),
|
||||
sp.GetRequiredService<IBootstrapService>(),
|
||||
sp.GetRequiredService<IAppInstaller>(),
|
||||
hardeningDir));
|
||||
builder.Services.AddScoped<WizardState>();
|
||||
|
||||
|
||||
@@ -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<AppCatalogEntry> Apps);
|
||||
|
||||
@@ -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<ApplyProgress> 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);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@using SilverOS.Welcome.Core.Apps
|
||||
@inject IApplyService ApplyService
|
||||
@inject IAppCatalog AppCatalog
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step apply-step">
|
||||
@@ -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<ApplyProgress>(p =>
|
||||
{
|
||||
|
||||
@@ -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<string>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var installer = new Mock<IAppInstaller>();
|
||||
installer.Setup(i => i.InstallAsync(
|
||||
It.IsAny<IReadOnlyList<AppCatalogEntry>>(),
|
||||
It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<AppInstallResult>());
|
||||
|
||||
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<AppCatalogEntry>());
|
||||
|
||||
// ---- Act ----
|
||||
await sut.RunAsync(req, new Progress<ApplyProgress>(_ => { }));
|
||||
|
||||
@@ -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<IAppInstaller> NoApps()
|
||||
{
|
||||
var installer = new Mock<IAppInstaller>();
|
||||
installer.Setup(i => i.InstallAsync(It.IsAny<IReadOnlyList<AppCatalogEntry>>(),
|
||||
It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<AppInstallResult>());
|
||||
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<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.TearDownAsync(It.IsAny<string>(),It.IsAny<CancellationToken>())).Callback(() => order.Add("bootstrap")).Returns(Task.CompletedTask);
|
||||
var installer = NoApps(); installer.Setup(i => i.InstallAsync(It.IsAny<IReadOnlyList<AppCatalogEntry>>(),It.IsAny<IProgress<ApplyProgress>>(),It.IsAny<CancellationToken>())).Callback(() => order.Add("apps")).ReturnsAsync(Array.Empty<AppInstallResult>());
|
||||
|
||||
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<AppCatalogEntry>());
|
||||
var progress = new List<string>();
|
||||
|
||||
await sut.RunAsync(req, new Progress<ApplyProgress>(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<IProcessRunner>(); run.Setup(r => r.RunAsync(It.IsAny<string>(),It.IsAny<string>(),It.IsAny<CancellationToken>())).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>())).ThrowsAsync(new InvalidOperationException("boom"));
|
||||
var bl = new Mock<IBitLockerService>(); var boot = new Mock<IBootstrapService>();
|
||||
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<AppCatalogEntry>());
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.RunAsync(req, new Progress<ApplyProgress>(_ => {})));
|
||||
boot.Verify(b => b.TearDownAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
@@ -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<IAppCatalog>(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<ApplyStep>();
|
||||
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<ApplyStep>(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<ApplyStep>();
|
||||
await cut.InvokeAsync(() => cut.Instance.StartAsync());
|
||||
Assert.Contains("Module 03 failed", cut.Markup);
|
||||
|
||||
@@ -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<IAppInstaller>();
|
||||
installer.Setup(i => i.InstallAsync(It.IsAny<IReadOnlyList<AppCatalogEntry>>(),
|
||||
It.IsAny<IProgress<ApplyProgress>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(System.Array.Empty<AppInstallResult>());
|
||||
|
||||
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<AppCatalogEntry>());
|
||||
|
||||
await sut.RunAsync(req, new Progress<ApplyProgress>(_ => { }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user