From 3b765d2fa1562a82b9950722f281bd091b23a1fe Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Wed, 29 Apr 2026 22:22:11 -0500 Subject: [PATCH] authoring app tests --- .../Covenant.Setup.Authoring.Tests.csproj | 23 ++++ authoring-ui.Tests/CovenantSetupToolTests.cs | 86 ++++++++++++++ authoring-ui.Tests/MainViewModelTests.cs | 74 ++++++++++++ authoring-ui.Tests/ManifestTomlWriterTests.cs | 108 ++++++++++++++++++ authoring-ui/Covenant.Setup.Authoring.csproj | 3 + authoring-ui/CovenantSetupTool.cs | 51 ++++++--- authoring-ui/MainViewModel.cs | 9 +- authoring-ui/README.md | 6 + 8 files changed, 342 insertions(+), 18 deletions(-) create mode 100644 authoring-ui.Tests/Covenant.Setup.Authoring.Tests.csproj create mode 100644 authoring-ui.Tests/CovenantSetupToolTests.cs create mode 100644 authoring-ui.Tests/MainViewModelTests.cs create mode 100644 authoring-ui.Tests/ManifestTomlWriterTests.cs diff --git a/authoring-ui.Tests/Covenant.Setup.Authoring.Tests.csproj b/authoring-ui.Tests/Covenant.Setup.Authoring.Tests.csproj new file mode 100644 index 0000000..c495156 --- /dev/null +++ b/authoring-ui.Tests/Covenant.Setup.Authoring.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0-windows10.0.19041.0 + enable + enable + false + true + Covenant.Setup.Authoring.Tests + Covenant.Setup.Authoring.Tests + + + + + + + + + + + + + diff --git a/authoring-ui.Tests/CovenantSetupToolTests.cs b/authoring-ui.Tests/CovenantSetupToolTests.cs new file mode 100644 index 0000000..16802a3 --- /dev/null +++ b/authoring-ui.Tests/CovenantSetupToolTests.cs @@ -0,0 +1,86 @@ +using Covenant.Setup.Authoring; +using Xunit; + +namespace Covenant.Setup.Authoring.Tests; + +public class CovenantSetupToolTests +{ + [Fact] + public void Find_returns_first_existing_covenant_setup_exe() + { + using var temp = new TempDirectory(); + var missing = Path.Combine(temp.Path, "missing"); + var found = Path.Combine(temp.Path, "found"); + Directory.CreateDirectory(missing); + Directory.CreateDirectory(found); + var exe = Path.Combine(found, "covenant-setup.exe"); + File.WriteAllText(exe, string.Empty); + + var tool = CovenantSetupToolLocator.Find([missing, found]); + + Assert.NotNull(tool); + Assert.Equal(exe, tool.Path); + } + + [Fact] + public void Find_returns_null_when_no_candidate_contains_exe() + { + using var temp = new TempDirectory(); + var candidate = Path.Combine(temp.Path, "candidate"); + Directory.CreateDirectory(candidate); + + var tool = CovenantSetupToolLocator.Find([candidate]); + + Assert.Null(tool); + } + + [Fact] + public void CreateStartInfo_builds_expected_package_command() + { + var tool = new CovenantSetupTool(@"C:\tools\covenant-setup.exe"); + var manifest = @"C:\src\sample app\install.toml"; + var output = @"C:\out dir"; + + var startInfo = CovenantSetupPackager.CreateStartInfo(tool, manifest, output); + + Assert.Equal(tool.Path, startInfo.FileName); + Assert.Equal(@"C:\src\sample app", startInfo.WorkingDirectory); + Assert.False(startInfo.UseShellExecute); + Assert.True(startInfo.CreateNoWindow); + Assert.True(startInfo.RedirectStandardOutput); + Assert.True(startInfo.RedirectStandardError); + Assert.Equal(["--json", "package", manifest, "--output", output], startInfo.ArgumentList); + } + + [Fact] + public void CreateStartInfo_uses_current_directory_for_relative_manifest() + { + var tool = new CovenantSetupTool(@"C:\tools\covenant-setup.exe"); + + var startInfo = CovenantSetupPackager.CreateStartInfo(tool, "install.toml", "dist"); + + Assert.Equal(Environment.CurrentDirectory, startInfo.WorkingDirectory); + } +} + +internal sealed class TempDirectory : IDisposable +{ + public TempDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "covenant-authoring-tests-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + { + try + { + Directory.Delete(Path, recursive: true); + } + catch + { + } + } +} diff --git a/authoring-ui.Tests/MainViewModelTests.cs b/authoring-ui.Tests/MainViewModelTests.cs new file mode 100644 index 0000000..1354751 --- /dev/null +++ b/authoring-ui.Tests/MainViewModelTests.cs @@ -0,0 +1,74 @@ +using Covenant.Setup.Authoring; +using Xunit; + +namespace Covenant.Setup.Authoring.Tests; + +public class MainViewModelTests +{ + [Fact] + public void Constructor_populates_default_manifest_entries() + { + var viewModel = new MainViewModel(() => null); + + Assert.Equal("Covenant-Setup Sample App", viewModel.AppName); + Assert.Contains(viewModel.Directories, directory => directory.Path == @"{LocalAppData}\CovenantSetupSample"); + Assert.Contains(viewModel.Files, file => file.Source == @"payload\sample_app.cmd"); + Assert.Contains(viewModel.PurgePaths, path => path == @"{LocalAppData}\CovenantSetupSample"); + Assert.Contains("app_name = \"Covenant-Setup Sample App\"", viewModel.TomlPreview); + } + + [Fact] + public void Validate_rejects_unsupported_registry_roots() + { + var viewModel = new MainViewModel(() => null); + viewModel.Registry.Clear(); + viewModel.PurgeRegistryBranches.Clear(); + viewModel.AddRegistry(@"HKCR\Software\Bad", "Name", "Value"); + viewModel.AddPurgeRegistryBranch(@"HKU\Software\Bad"); + + var validation = viewModel.Validate(); + + Assert.False(validation.IsValid); + Assert.Contains(validation.Errors, error => error.Contains(@"HKCR\Software\Bad", StringComparison.Ordinal)); + Assert.Contains(validation.Errors, error => error.Contains(@"HKU\Software\Bad", StringComparison.Ordinal)); + } + + [Fact] + public void Validate_warns_when_manifest_requires_elevation() + { + var viewModel = new MainViewModel(() => null) + { + InstallRootToken = "{ProgramFilesX64}" + }; + viewModel.ApplyDefaults(resetCollections: true); + + var validation = viewModel.Validate(); + + Assert.True(validation.IsValid); + Assert.Contains(validation.Warnings, warning => warning.Contains("require elevation", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void Packaging_is_disabled_when_covenant_setup_is_missing() + { + var viewModel = new MainViewModel(() => null); + + Assert.False(viewModel.HasCovenantSetupTool); + Assert.False(viewModel.CanPackage); + Assert.Contains("not found", viewModel.CovenantSetupStatus, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Packaging_is_enabled_only_when_tool_and_valid_manifest_are_present() + { + var viewModel = new MainViewModel(() => new CovenantSetupTool(@"C:\tools\covenant-setup.exe")); + + Assert.True(viewModel.HasCovenantSetupTool); + Assert.True(viewModel.CanPackage); + + viewModel.AppName = string.Empty; + + Assert.True(viewModel.HasValidationErrors); + Assert.False(viewModel.CanPackage); + } +} diff --git a/authoring-ui.Tests/ManifestTomlWriterTests.cs b/authoring-ui.Tests/ManifestTomlWriterTests.cs new file mode 100644 index 0000000..b64493a --- /dev/null +++ b/authoring-ui.Tests/ManifestTomlWriterTests.cs @@ -0,0 +1,108 @@ +using Covenant.Setup.Authoring; +using Xunit; + +namespace Covenant.Setup.Authoring.Tests; + +public class ManifestTomlWriterTests +{ + [Fact] + public void Write_emits_current_manifest_schema() + { + var document = new ManifestDocument + { + AppName = "Sample App", + Directories = + [ + new DirectorySpec(@"{LocalAppData}\Sample") + ], + Files = + [ + new FileSpec(@"payload\app.exe", @"{LocalAppData}\Sample\bin\app.exe") + ], + Registry = + [ + new RegistrySpec(@"HKCU\Software\Sample", "InstallRoot", @"{LocalAppData}\Sample") + ], + Shortcuts = + [ + new ShortcutSpec( + @"{Desktop}\Sample App.lnk", + @"{LocalAppData}\Sample\bin\app.exe", + "--profile default", + @"{LocalAppData}\Sample", + "Launch Sample App") + ], + Scripts = + [ + new ScriptSpec( + "powershell", + ["-ExecutionPolicy", "Bypass", "-File", @"payload\post_install.ps1"], + @"{LocalAppData}\Sample") + ], + Purge = new PurgeSpec + { + RegistryBranches = [@"HKCU\Software\Sample"], + Paths = [@"{LocalAppData}\Sample"] + } + }; + + var toml = Normalize(ManifestTomlWriter.Write(document)); + + const string expected = """ +app_name = "Sample App" + +[[directories]] +path = "{LocalAppData}\\Sample" + +[[files]] +source = "payload\\app.exe" +destination = "{LocalAppData}\\Sample\\bin\\app.exe" + +[[registry]] +key = "HKCU\\Software\\Sample" +name = "InstallRoot" +value = "{LocalAppData}\\Sample" + +[[shortcuts]] +path = "{Desktop}\\Sample App.lnk" +target = "{LocalAppData}\\Sample\\bin\\app.exe" +arguments = "--profile default" +working_directory = "{LocalAppData}\\Sample" +description = "Launch Sample App" + +[[scripts]] +command = "powershell" +args = [ + "-ExecutionPolicy", + "Bypass", + "-File", + "payload\\post_install.ps1" +] +working_directory = "{LocalAppData}\\Sample" + +[purge] +registry_branches = ["HKCU\\Software\\Sample"] +paths = ["{LocalAppData}\\Sample"] + +"""; + Assert.Equal(Normalize(expected), toml); + } + + [Fact] + public void Write_escapes_toml_strings() + { + var document = new ManifestDocument + { + AppName = "Quoted \"App\"\nTabbed\tPath", + Directories = [new DirectorySpec(@"C:\Apps\Sample")], + Purge = new PurgeSpec() + }; + + var toml = Normalize(ManifestTomlWriter.Write(document)); + + Assert.Contains("app_name = \"Quoted \\\"App\\\"\\nTabbed\\tPath\"", toml); + Assert.Contains("path = \"C:\\\\Apps\\\\Sample\"", toml); + } + + private static string Normalize(string value) => value.Replace("\r\n", "\n"); +} diff --git a/authoring-ui/Covenant.Setup.Authoring.csproj b/authoring-ui/Covenant.Setup.Authoring.csproj index 8edd04a..71d81cb 100644 --- a/authoring-ui/Covenant.Setup.Authoring.csproj +++ b/authoring-ui/Covenant.Setup.Authoring.csproj @@ -18,4 +18,7 @@ + + + diff --git a/authoring-ui/CovenantSetupTool.cs b/authoring-ui/CovenantSetupTool.cs index cdbd7f9..27cd4ca 100644 --- a/authoring-ui/CovenantSetupTool.cs +++ b/authoring-ui/CovenantSetupTool.cs @@ -15,7 +15,12 @@ internal static class CovenantSetupToolLocator { public static CovenantSetupTool? Find() { - foreach (var candidate in CandidatePaths()) + return Find(CandidateDirectories()); + } + + internal static CovenantSetupTool? Find(IEnumerable candidateDirectories) + { + foreach (var candidate in CandidatePaths(candidateDirectories)) { if (File.Exists(candidate)) { @@ -26,10 +31,10 @@ internal static class CovenantSetupToolLocator return null; } - private static IEnumerable CandidatePaths() + private static IEnumerable CandidatePaths(IEnumerable candidateDirectories) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var directory in CandidateDirectories()) + foreach (var directory in candidateDirectories) { var candidate = System.IO.Path.Combine(directory, "covenant-setup.exe"); if (seen.Add(candidate)) @@ -91,22 +96,9 @@ internal static class CovenantSetupPackager var error = new StringBuilder(); using var process = new Process { - StartInfo = new ProcessStartInfo - { - FileName = tool.Path, - WorkingDirectory = System.IO.Path.GetDirectoryName(manifestPath) ?? Environment.CurrentDirectory, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }, + StartInfo = CreateStartInfo(tool, manifestPath, outputDirectory), EnableRaisingEvents = true }; - process.StartInfo.ArgumentList.Add("--json"); - process.StartInfo.ArgumentList.Add("package"); - process.StartInfo.ArgumentList.Add(manifestPath); - process.StartInfo.ArgumentList.Add("--output"); - process.StartInfo.ArgumentList.Add(outputDirectory); process.OutputDataReceived += (_, args) => { @@ -133,4 +125,29 @@ internal static class CovenantSetupPackager output.ToString().Trim(), error.ToString().Trim()); } + + internal static ProcessStartInfo CreateStartInfo( + CovenantSetupTool tool, + string manifestPath, + string outputDirectory) + { + var manifestDirectory = System.IO.Path.GetDirectoryName(manifestPath); + var startInfo = new ProcessStartInfo + { + FileName = tool.Path, + WorkingDirectory = string.IsNullOrWhiteSpace(manifestDirectory) + ? Environment.CurrentDirectory + : manifestDirectory, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.ArgumentList.Add("--json"); + startInfo.ArgumentList.Add("package"); + startInfo.ArgumentList.Add(manifestPath); + startInfo.ArgumentList.Add("--output"); + startInfo.ArgumentList.Add(outputDirectory); + return startInfo; + } } diff --git a/authoring-ui/MainViewModel.cs b/authoring-ui/MainViewModel.cs index b6b2bb0..29d6736 100644 --- a/authoring-ui/MainViewModel.cs +++ b/authoring-ui/MainViewModel.cs @@ -18,9 +18,16 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged private string _tomlPreview = string.Empty; private string _validationSummary = string.Empty; private bool _hasValidationErrors; + private readonly Func _locateCovenantSetupTool; public MainViewModel() + : this(CovenantSetupToolLocator.Find) { + } + + internal MainViewModel(Func locateCovenantSetupTool) + { + _locateCovenantSetupTool = locateCovenantSetupTool; Directories.CollectionChanged += CollectionChanged; Files.CollectionChanged += CollectionChanged; Registry.CollectionChanged += CollectionChanged; @@ -170,7 +177,7 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged public void RefreshCovenantSetupTool() { - CovenantSetupTool = CovenantSetupToolLocator.Find(); + CovenantSetupTool = _locateCovenantSetupTool(); CovenantSetupStatus = CovenantSetupTool is null ? "covenant-setup.exe was not found. Packaging is disabled." : "Packaging enabled: " + CovenantSetupTool.Path; diff --git a/authoring-ui/README.md b/authoring-ui/README.md index d6b55bb..8da2157 100644 --- a/authoring-ui/README.md +++ b/authoring-ui/README.md @@ -8,6 +8,12 @@ Run from the repository root: dotnet run --project .\authoring-ui\Covenant.Setup.Authoring.csproj ``` +Run unit tests from the repository root: + +```powershell +dotnet test .\authoring-ui.Tests\Covenant.Setup.Authoring.Tests.csproj +``` + The app edits the current manifest schema: - `app_name`