diff --git a/SysManager/SysManager.Tests/BulkInstallerServiceTests.cs b/SysManager/SysManager.Tests/BulkInstallerServiceTests.cs
new file mode 100644
index 0000000..dade044
--- /dev/null
+++ b/SysManager/SysManager.Tests/BulkInstallerServiceTests.cs
@@ -0,0 +1,122 @@
+// SysManager · BulkInstallerServiceTests
+// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
+// License: MIT
+
+using NSubstitute;
+using SysManager.Services;
+
+namespace SysManager.Tests;
+
+///
+/// Tests for (audit finding tests #4, #9).
+///
+/// The service is the only barrier between preset/user package IDs and a
+/// shelled-out winget process: InstallAsync rejects anything that
+/// fails PackageIdPattern before any process launches, preventing
+/// command injection into winget arguments. These tests pin that guard and
+/// assert the exact winget invocation on the happy path — using the
+/// seam so no winget process ever runs.
+///
+///
+public class BulkInstallerServiceTests
+{
+ // ---------- injection / validation guard (#4) ----------
+
+ public static IEnumerable InvalidPackageIds()
+ {
+ yield return ["App & calc.exe"]; // command chaining via &
+ yield return ["App; calc.exe"]; // command separator
+ yield return ["App | calc.exe"]; // pipe
+ yield return ["App\"--evil"]; // quote break-out
+ yield return ["App`whoami`"]; // backtick subexpression
+ yield return ["App$(whoami)"]; // $() subexpression
+ yield return ["App\nGit.Git"]; // newline injection
+ yield return [new string('A', 300)]; // exceeds the 256-char cap
+ yield return [" "]; // whitespace only
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidPackageIds))]
+ public async Task InstallAsync_InvalidId_ThrowsArgumentException_AndNeverRunsProcess(string badId)
+ {
+ var runner = Substitute.For();
+ var svc = new BulkInstallerService(runner);
+
+ await Assert.ThrowsAsync(() => svc.InstallAsync(badId));
+
+ // The guard runs before any process launch — the runner must never be called.
+ await runner.DidNotReceiveWithAnyArgs()
+ .RunProcessAsync(default!, default!, default, default);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public async Task InstallAsync_NullOrEmptyId_ThrowsArgumentException(string? badId)
+ {
+ var runner = Substitute.For();
+ var svc = new BulkInstallerService(runner);
+
+ await Assert.ThrowsAsync(() => svc.InstallAsync(badId!));
+ }
+
+ // ---------- happy path: exact winget invocation (#9) ----------
+
+ [Fact]
+ public async Task InstallAsync_ValidId_InvokesWingetWithExpectedArgs()
+ {
+ var runner = Substitute.For();
+ runner.RunProcessAsync("winget", Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(0);
+ var svc = new BulkInstallerService(runner);
+
+ var exit = await svc.InstallAsync("Git.Git");
+
+ Assert.Equal(0, exit);
+ await runner.Received(1).RunProcessAsync(
+ "winget",
+ Arg.Is(a =>
+ a.Contains("install") &&
+ a.Contains("--id \"Git.Git\"") &&
+ a.Contains("-e") &&
+ a.Contains("--silent") &&
+ a.Contains("--accept-source-agreements") &&
+ a.Contains("--accept-package-agreements")),
+ Arg.Any(),
+ Arg.Any());
+ }
+
+ [Theory]
+ [InlineData("7zip.7zip")]
+ [InlineData("Mozilla.Firefox")]
+ [InlineData("Valve.Steam")]
+ public async Task InstallAsync_AcceptsValidPublicIds(string id)
+ {
+ var runner = Substitute.For();
+ runner.RunProcessAsync("winget", Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(0);
+ var svc = new BulkInstallerService(runner);
+
+ var exit = await svc.InstallAsync(id);
+
+ Assert.Equal(0, exit);
+ await runner.Received(1).RunProcessAsync(
+ "winget",
+ Arg.Is(a => a.Contains($"--id \"{id}\"")),
+ Arg.Any(),
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task InstallAsync_PropagatesNonZeroExitCode()
+ {
+ var runner = Substitute.For();
+ runner.RunProcessAsync("winget", Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(1);
+ var svc = new BulkInstallerService(runner);
+
+ var exit = await svc.InstallAsync("Git.Git");
+
+ Assert.Equal(1, exit);
+ }
+}
diff --git a/SysManager/SysManager.Tests/DnsServiceTests.cs b/SysManager/SysManager.Tests/DnsServiceTests.cs
new file mode 100644
index 0000000..6522851
--- /dev/null
+++ b/SysManager/SysManager.Tests/DnsServiceTests.cs
@@ -0,0 +1,159 @@
+// SysManager · DnsServiceTests
+// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
+// License: MIT
+
+using System.Collections.ObjectModel;
+using System.Management.Automation;
+using NSubstitute;
+using SysManager.Services;
+
+namespace SysManager.Tests;
+
+///
+/// Tests for (audit finding tests #5).
+///
+/// SetDnsAsync validates both addresses with IPAddress.TryParse
+/// and throws before any PowerShell runs — the
+/// guard that stops a malformed or injected value from reaching
+/// Set-DnsClientServerAddress . The interface index (an integer) is used
+/// rather than the adapter name to avoid command injection. These tests pin the
+/// validation guard and assert the exact script on the happy path via the
+/// seam, so no live DNS state is touched.
+///
+///
+public class DnsServiceTests
+{
+ private static Collection Result(string value) =>
+ new() { PSObject.AsPSObject(value) };
+
+ // ---------- IP-validation guard (#5) ----------
+
+ public static IEnumerable InvalidAddresses()
+ {
+ yield return ["not-an-ip"];
+ yield return ["8.8.8.8; calc.exe"]; // injection attempt
+ yield return ["8.8.8.8\")"]; // quote/paren break-out
+ yield return ["999.999.999.999"]; // out-of-range octets
+ yield return [""];
+ yield return [" "];
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidAddresses))]
+ public async Task SetDnsAsync_InvalidPrimary_ThrowsArgumentException_AndNeverRunsScript(string badPrimary)
+ {
+ var runner = Substitute.For();
+ using var svc = new DnsService(runner);
+
+ await Assert.ThrowsAsync(() => svc.SetDnsAsync(badPrimary, "8.8.4.4"));
+
+ await runner.DidNotReceiveWithAnyArgs().RunAsync(default!, default, default);
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidAddresses))]
+ public async Task SetDnsAsync_InvalidSecondary_ThrowsArgumentException(string badSecondary)
+ {
+ var runner = Substitute.For();
+ using var svc = new DnsService(runner);
+
+ await Assert.ThrowsAsync(() => svc.SetDnsAsync("8.8.8.8", badSecondary));
+ }
+
+ // ---------- happy path: exact Set script via the seam ----------
+
+ [Fact]
+ public async Task SetDnsAsync_ValidAddresses_RunsSetScriptWithThoseAddresses()
+ {
+ var runner = Substitute.For();
+ // First RunAsync resolves the active interface index; subsequent calls
+ // (the Set script) can return anything — the result is not consumed.
+ runner.RunAsync(Arg.Any(), Arg.Any?>(), Arg.Any())
+ .Returns(Result("5"));
+ using var svc = new DnsService(runner);
+
+ await svc.SetDnsAsync("1.1.1.1", "1.0.0.1");
+
+ await runner.Received(1).RunAsync(
+ Arg.Is(s =>
+ s.Contains("Set-DnsClientServerAddress") &&
+ s.Contains("-InterfaceIndex 5") &&
+ s.Contains("1.1.1.1") &&
+ s.Contains("1.0.0.1")),
+ Arg.Any?>(),
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task ResetToDhcpAsync_RunsResetScript()
+ {
+ var runner = Substitute.For();
+ runner.RunAsync(Arg.Any(), Arg.Any?>(), Arg.Any())
+ .Returns(Result("3"));
+ using var svc = new DnsService(runner);
+
+ await svc.ResetToDhcpAsync();
+
+ await runner.Received(1).RunAsync(
+ Arg.Is(s =>
+ s.Contains("Set-DnsClientServerAddress") &&
+ s.Contains("-InterfaceIndex 3") &&
+ s.Contains("-ResetServerAddresses")),
+ Arg.Any?>(),
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task GetCurrentDnsAsync_ReturnsFirstResultLine()
+ {
+ var runner = Substitute.For();
+ runner.RunAsync(Arg.Any(), Arg.Any?>(), Arg.Any())
+ .Returns(Result("8.8.8.8, 8.8.4.4"));
+ using var svc = new DnsService(runner);
+
+ var current = await svc.GetCurrentDnsAsync();
+
+ Assert.Equal("8.8.8.8, 8.8.4.4", current);
+ }
+
+ [Fact]
+ public async Task GetCurrentDnsAsync_NoResults_ReturnsUnknown()
+ {
+ var runner = Substitute.For();
+ runner.RunAsync(Arg.Any(), Arg.Any?>(), Arg.Any())
+ .Returns(new Collection());
+ using var svc = new DnsService(runner);
+
+ var current = await svc.GetCurrentDnsAsync();
+
+ Assert.Equal("Unknown", current);
+ }
+
+ // ---------- presets (pure) ----------
+
+ [Fact]
+ public void GetPresets_IncludesGoogleCloudflareQuad9OpenDnsAndAutomatic()
+ {
+ using var svc = new DnsService(Substitute.For());
+
+ var presets = svc.GetPresets();
+ var names = presets.Select(p => p.Name).ToList();
+
+ Assert.Contains("Google", names);
+ Assert.Contains("Cloudflare", names);
+ Assert.Contains("Quad9", names);
+ Assert.Contains("OpenDNS", names);
+ Assert.Contains(names, n => n.Contains("Automatic", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public void GetPresets_GoogleHasExpectedAddresses()
+ {
+ using var svc = new DnsService(Substitute.For());
+
+ var google = svc.GetPresets().First(p => p.Name == "Google");
+
+ Assert.Equal("8.8.8.8", google.Primary);
+ Assert.Equal("8.8.4.4", google.Secondary);
+ }
+}
diff --git a/SysManager/SysManager.Tests/NetworkRepairServiceTests.cs b/SysManager/SysManager.Tests/NetworkRepairServiceTests.cs
new file mode 100644
index 0000000..9050c86
--- /dev/null
+++ b/SysManager/SysManager.Tests/NetworkRepairServiceTests.cs
@@ -0,0 +1,128 @@
+// SysManager · NetworkRepairServiceTests
+// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
+// License: MIT
+
+using NSubstitute;
+using SysManager.Services;
+
+namespace SysManager.Tests;
+
+///
+/// Tests for (audit finding tests #8).
+///
+/// Each repair routes a fixed command through the
+/// seam (ipconfig /flushdns , netsh winsock reset ,
+/// netsh int ip reset ) and maps the exit code plus a fixed reboot flag
+/// into a . These tests pin
+/// the exact invocation and the Success/NeedsReboot mapping with zero OS
+/// interaction by substituting the runner.
+///
+///
+public class NetworkRepairServiceTests
+{
+ private static IPowerShellRunner RunnerReturning(int exitCode)
+ {
+ var runner = Substitute.For();
+ runner.RunProcessAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(exitCode);
+ return runner;
+ }
+
+ // ---------- DNS flush — no reboot ----------
+
+ [Fact]
+ public async Task FlushDnsAsync_RunsIpconfigFlushDns()
+ {
+ var runner = RunnerReturning(0);
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.FlushDnsAsync();
+
+ await runner.Received(1).RunProcessAsync(
+ "ipconfig.exe", "/flushdns", Arg.Any(), Arg.Any());
+ Assert.True(result.Success);
+ Assert.False(result.NeedsReboot);
+ Assert.Equal("DNS Flush", result.ToolName);
+ }
+
+ // ---------- Winsock reset — needs reboot ----------
+
+ [Fact]
+ public async Task ResetWinsockAsync_RunsNetshWinsockReset_NeedsReboot()
+ {
+ var runner = RunnerReturning(0);
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.ResetWinsockAsync();
+
+ await runner.Received(1).RunProcessAsync(
+ "netsh.exe", "winsock reset", Arg.Any(), Arg.Any());
+ Assert.True(result.Success);
+ Assert.True(result.NeedsReboot);
+ Assert.Equal("Winsock Reset", result.ToolName);
+ }
+
+ // ---------- TCP/IP reset — needs reboot ----------
+
+ [Fact]
+ public async Task ResetTcpIpAsync_RunsNetshIntIpReset_NeedsReboot()
+ {
+ var runner = RunnerReturning(0);
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.ResetTcpIpAsync();
+
+ await runner.Received(1).RunProcessAsync(
+ "netsh.exe", "int ip reset", Arg.Any(), Arg.Any());
+ Assert.True(result.Success);
+ Assert.True(result.NeedsReboot);
+ Assert.Equal("TCP/IP Reset", result.ToolName);
+ }
+
+ // ---------- non-zero exit maps to failure but keeps the reboot flag ----------
+
+ [Fact]
+ public async Task FlushDnsAsync_NonZeroExit_ReportsFailure()
+ {
+ var runner = RunnerReturning(1);
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.FlushDnsAsync();
+
+ Assert.False(result.Success);
+ Assert.False(result.NeedsReboot);
+ }
+
+ [Fact]
+ public async Task ResetWinsockAsync_NonZeroExit_FailsButStillFlagsReboot()
+ {
+ var runner = RunnerReturning(1);
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.ResetWinsockAsync();
+
+ Assert.False(result.Success);
+ Assert.True(result.NeedsReboot); // reboot flag is intrinsic to the operation, not the outcome
+ }
+
+ // ---------- streamed output is collected into the result ----------
+
+ [Fact]
+ public async Task FlushDnsAsync_CollectsStreamedOutputLines()
+ {
+ var runner = Substitute.For();
+ runner.RunProcessAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(_ =>
+ {
+ // Simulate the runner streaming a line mid-execution.
+ runner.LineReceived += Raise.Event>(
+ SysManager.Models.PowerShellLine.Output("Successfully flushed the DNS Resolver Cache."));
+ return 0;
+ });
+ using var svc = new NetworkRepairService(runner);
+
+ var result = await svc.FlushDnsAsync();
+
+ Assert.Contains("flushed", result.Output, StringComparison.OrdinalIgnoreCase);
+ }
+}