122 lines
5.5 KiB
C#
122 lines
5.5 KiB
C#
using Moq;
|
|
using SilverOS.Welcome.Core.Apply;
|
|
using SilverOS.Welcome.Core.Apps;
|
|
using SilverOS.Welcome.Core.Flavours;
|
|
using Xunit;
|
|
|
|
/// <summary>
|
|
/// Real integration test: proves that ApplyService passes -Modules with the correct
|
|
/// encoding so that Invoke-Hardening.ps1's subset filter actually works through the
|
|
/// real ProcessStartInfo / PowerShell boundary.
|
|
///
|
|
/// SAFETY: only harmless dummy .ps1 files are executed — never the real 0*.ps1 hardening
|
|
/// modules. Invoke-Hardening.ps1 is copied into a temp dir and run against dummy stubs.
|
|
/// </summary>
|
|
public class ApplyServiceHardeningIntegrationTests
|
|
{
|
|
/// <summary>Walk up from the test binary to find the repo root (same as ShippedFlavoursTests).</summary>
|
|
private static string HardeningDir()
|
|
{
|
|
var d = AppContext.BaseDirectory;
|
|
while (d is not null && !Directory.Exists(Path.Combine(d, "windows", "hardening")))
|
|
d = Directory.GetParent(d)?.FullName;
|
|
return Path.Combine(d!, "windows", "hardening");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Subset_filter_runs_only_requested_modules_via_real_powershell()
|
|
{
|
|
// ---- Arrange: set up a temp sandbox ----
|
|
var tmp = Path.Combine(Path.GetTempPath(), $"sm_integ_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tmp);
|
|
try
|
|
{
|
|
// Copy the REAL Invoke-Hardening.ps1 (the one we just patched) into the temp dir.
|
|
var realInvoke = Path.Combine(HardeningDir(), "Invoke-Hardening.ps1");
|
|
File.Copy(realInvoke, Path.Combine(tmp, "Invoke-Hardening.ps1"));
|
|
|
|
// Create harmless dummy module stubs. Each just appends its prefix to ran.txt.
|
|
var ranFile = Path.Combine(tmp, "ran.txt").Replace("\\", "\\\\");
|
|
foreach (var (prefix, name) in new[] {
|
|
("00", "00-a.ps1"),
|
|
("03", "03-b.ps1"),
|
|
("05", "05-c.ps1"),
|
|
("07", "07-d.ps1"),
|
|
})
|
|
{
|
|
// Single quotes around prefix so the string itself is written, not executed.
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(tmp, name),
|
|
$"'RAN {prefix}' | Out-File -Append \"{ranFile.Replace("\\\\", "\\\\")}\"");
|
|
}
|
|
|
|
// Dummy Verify script — no-op so Invoke-Hardening.ps1's Verify step succeeds.
|
|
await File.WriteAllTextAsync(
|
|
Path.Combine(tmp, "Verify-SilverMetalWindows.ps1"),
|
|
"# no-op verify");
|
|
|
|
// ---- Arrange: mocked services so apply completes without touching real OS ----
|
|
var acct = new Mock<IAccountService>();
|
|
acct.Setup(a => a.CreateAccountsAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var bl = new Mock<IBitLockerService>();
|
|
bl.Setup(b => b.EnableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var boot = new Mock<IBootstrapService>();
|
|
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.
|
|
var flavour = new FlavourManifest
|
|
{
|
|
Id = "test",
|
|
Hardening = new HardeningSpec { Modules = new[] { "00", "05" } }
|
|
};
|
|
var req = new ApplyRequest(flavour, "alice", "pw", "adminpw", "123456", "sm-bootstrap", Array.Empty<AppCatalogEntry>());
|
|
|
|
// ---- Act ----
|
|
await sut.RunAsync(req, new Progress<ApplyProgress>(_ => { }));
|
|
|
|
// ---- Assert: ran.txt should contain only 00 and 05 markers ----
|
|
Assert.True(File.Exists(Path.Combine(tmp, "ran.txt")),
|
|
"ran.txt was not created — no module ran at all (subset filter matched nothing)");
|
|
|
|
var ran = await File.ReadAllTextAsync(Path.Combine(tmp, "ran.txt"));
|
|
|
|
Assert.Contains("RAN 00", ran, StringComparison.Ordinal);
|
|
Assert.Contains("RAN 05", ran, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("RAN 03", ran, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("RAN 07", ran, StringComparison.Ordinal);
|
|
|
|
// ---- Assert: the rest of the apply pipeline also completed ----
|
|
acct.Verify(a => a.CreateAccountsAsync(
|
|
"alice", "pw", "adminpw", It.IsAny<CancellationToken>()), Times.Once);
|
|
bl.Verify(b => b.EnableAsync("123456", It.IsAny<CancellationToken>()), Times.Once);
|
|
boot.Verify(b => b.TearDownAsync("sm-bootstrap", It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
finally
|
|
{
|
|
// Clean up — ignore errors (locked files etc.) to avoid masking test failure.
|
|
try { Directory.Delete(tmp, recursive: true); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
}
|