authoring app tests

This commit is contained in:
2026-04-29 22:22:11 -05:00
parent bcad606fce
commit 3b765d2fa1
8 changed files with 342 additions and 18 deletions
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AssemblyName>Covenant.Setup.Authoring.Tests</AssemblyName>
<RootNamespace>Covenant.Setup.Authoring.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\authoring-ui\Covenant.Setup.Authoring.csproj" />
</ItemGroup>
</Project>
@@ -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
{
}
}
}
+74
View File
@@ -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);
}
}
@@ -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");
}
@@ -18,4 +18,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Covenant.Setup.Authoring.Tests" />
</ItemGroup>
</Project>
+34 -17
View File
@@ -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<string> candidateDirectories)
{
foreach (var candidate in CandidatePaths(candidateDirectories))
{
if (File.Exists(candidate))
{
@@ -26,10 +31,10 @@ internal static class CovenantSetupToolLocator
return null;
}
private static IEnumerable<string> CandidatePaths()
private static IEnumerable<string> CandidatePaths(IEnumerable<string> candidateDirectories)
{
var seen = new HashSet<string>(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;
}
}
+8 -1
View File
@@ -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<CovenantSetupTool?> _locateCovenantSetupTool;
public MainViewModel()
: this(CovenantSetupToolLocator.Find)
{
}
internal MainViewModel(Func<CovenantSetupTool?> 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;
+6
View File
@@ -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`