73 lines
10 KiB
C#
73 lines
10 KiB
C#
using System.Security.Cryptography;
|
|
using Launcher.Next.Autodesk;
|
|
using Launcher.Next.Core;
|
|
using Launcher.Next.Infrastructure;
|
|
using Xunit;
|
|
namespace Launcher.Next.Tests;
|
|
|
|
public sealed class FoundationTests
|
|
{
|
|
private static PackageManifest Manifest(params PackageFile[] files) => new(1, "P_Rec_Annot", "2026.1.0", "Plant3D", 2026, "x64", "PRecAnnot.Plant3D2026.dll", files, new("netload", "PHP_REC_ANNOT"));
|
|
private static PackageFile Package(string path, string hash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") => new(path, 1, hash);
|
|
[Fact] public void Manifest_ValidPackage_IsAccepted() => Assert.True(new PackageManifestValidator().Validate(Manifest(Package("PRecAnnot.Plant3D2026.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_PathTraversal_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("../evil.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_AbsolutePath_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("C:\\evil.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_MissingEntryAssembly_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("Other.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_AutodeskAssembly_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("PRecAnnot.Plant3D2026.dll"), Package("AcMgd.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_DuplicateFile_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("PRecAnnot.Plant3D2026.dll"), Package("PRecAnnot.Plant3D2026.dll")), 2026, "x64").IsValid);
|
|
[Fact] public void Manifest_WrongArchitecture_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("PRecAnnot.Plant3D2026.dll")), 2026, "x86").IsValid);
|
|
[Fact] public void Manifest_WrongProductYear_IsRejected() => Assert.False(new PackageManifestValidator().Validate(Manifest(Package("PRecAnnot.Plant3D2026.dll")), 2025, "x64").IsValid);
|
|
[Fact] public async Task Synchronizer_UsesStaging_AndRetainsPreviousVersion()
|
|
{ var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var bytes = new byte[] { 7 }; await System.IO.File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var hash = Convert.ToHexString(SHA256.HashData(bytes)); var store = new LocalPackageStore(root, new Sha256FileHasher()); var result = await store.SynchronizeAsync(Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 1, hash)), source, CancellationToken.None); Assert.Equal("2026.1.0", result.CurrentVersion); Assert.True(Directory.Exists(Path.Combine(root, "Applications", "P_Rec_Annot", "versions", result.CurrentVersion))); Directory.Delete(root, true); }
|
|
[Fact] public async Task Synchronizer_HashMismatch_IsRejected()
|
|
{ var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); await System.IO.File.WriteAllTextAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), "x"); var store = new LocalPackageStore(root, new Sha256FileHasher()); await Assert.ThrowsAsync<InvalidOperationException>(() => store.SynchronizeAsync(Manifest(Package("PRecAnnot.Plant3D2026.dll")), source, CancellationToken.None)); Assert.False(Directory.Exists(Path.Combine(root, "Applications", "P_Rec_Annot", "versions", "2026.1.0"))); Directory.Delete(root, true); }
|
|
[Fact] public async Task LastKnownGood_RejectsCorruptCurrent_AndUsesVerifiedPreviousOffline()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var store = new LocalPackageStore(root, new Sha256FileHasher());
|
|
try
|
|
{
|
|
await System.IO.File.WriteAllTextAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), "version-one"); var firstHash = Convert.ToHexString(SHA256.HashData(await System.IO.File.ReadAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll")))); await store.SynchronizeAsync(Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 11, firstHash)), source, CancellationToken.None);
|
|
await System.IO.File.WriteAllTextAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), "version-two"); var secondHash = Convert.ToHexString(SHA256.HashData(await System.IO.File.ReadAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll")))); var second = Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 11, secondHash)) with { Version = "2026.2.0" }; await store.SynchronizeAsync(second, source, CancellationToken.None);
|
|
await System.IO.File.WriteAllTextAsync(Path.Combine(root, "Applications", "P_Rec_Annot", "versions", "2026.2.0", "PRecAnnot.Plant3D2026.dll"), "corrupt");
|
|
var resolved = await store.ResolveAsync("P_Rec_Annot", "Plant3D", 2026, "x64", true, CancellationToken.None);
|
|
Assert.NotNull(resolved); Assert.Equal("2026.1.0", resolved!.Version); Assert.True(resolved.IsOfflineFallback); Assert.Equal("PREVIOUS_VERIFIED_AFTER_CURRENT_REJECTED", resolved.Reason);
|
|
}
|
|
finally { Directory.Delete(root, true); }
|
|
}
|
|
[Fact] public async Task LastKnownGood_DoesNotFallback_WhenOfflinePolicyIsDisabled()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var bytes = new byte[] { 1 }; await System.IO.File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var hash = Convert.ToHexString(SHA256.HashData(bytes)); var store = new LocalPackageStore(root, new Sha256FileHasher());
|
|
try { await store.SynchronizeAsync(Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 1, hash)), source, CancellationToken.None); await store.SynchronizeAsync((Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 1, hash)) with { Version = "2026.2.0" }), source, CancellationToken.None); System.IO.File.Delete(Path.Combine(root, "Applications", "P_Rec_Annot", "versions", "2026.2.0", "PRecAnnot.Plant3D2026.dll")); Assert.Null(await store.ResolveAsync("P_Rec_Annot", "Plant3D", 2026, "x64", false, CancellationToken.None)); }
|
|
finally { Directory.Delete(root, true); }
|
|
}
|
|
[Fact] public async Task Synchronizer_RejectsReservedStoredManifestPayload()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var bytes = new byte[] { 1 }; await System.IO.File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var hash = Convert.ToHexString(SHA256.HashData(bytes));
|
|
try { var store = new LocalPackageStore(root, new Sha256FileHasher()); var manifest = Manifest(new PackageFile("PRecAnnot.Plant3D2026.dll", 1, hash), new PackageFile("package.manifest.json", 1, hash)); var error = await Assert.ThrowsAsync<InvalidOperationException>(() => store.SynchronizeAsync(manifest, source, CancellationToken.None)); Assert.Equal("MANIFEST_RESERVED_FILE", error.Message); }
|
|
finally { Directory.Delete(root, true); }
|
|
}
|
|
[Fact] public void StartupScript_ContainsOneNetload_AndEscapesSpaces()
|
|
{ var root = Path.Combine(Path.GetTempPath(), "space root", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); System.IO.File.WriteAllText(Path.Combine(root, "PRecAnnot.Plant3D2026.dll"), ""); var s = new StartupScriptBuilder().BuildNetloadScript(root, "PRecAnnot.Plant3D2026.dll"); Assert.Single(System.Text.RegularExpressions.Regex.Matches(s, "netload", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Cast<object>()); Assert.Contains("space root", s); Directory.Delete(Path.GetDirectoryName(root)!, true); }
|
|
[Fact] public void ProcessLauncher_DryRun_DoesNotStartProcess()
|
|
{ var p = new DryRunAutodeskProcessLauncher().Prepare(new LaunchRequest(new ProductInstallation("C:\\acad.exe", "2026", "x64", []), "Pilot", "C:\\script.scr", true)); Assert.False(p.WouldStart); }
|
|
[Fact] public void Core_HasNoAutodeskOrProcessReferences() => Assert.DoesNotContain(typeof(PackageManifest).Assembly.GetReferencedAssemblies(), x => x.Name!.Contains("Autodesk") || x.Name.Contains("Diagnostics.Process"));
|
|
[Fact] public async Task PRecAnnotPilot_DryRun_ValidatesActivatesAndPreparesWithoutStarting()
|
|
{
|
|
var fixture = await System.IO.File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "fixtures", "p-rec-annot-pilot.manifest.json"));
|
|
var parsed = PackageManifestJson.Parse(fixture); var bytes = new byte[] { 7 }; var hash = Convert.ToHexString(SHA256.HashData(bytes));
|
|
var manifest = parsed with { Files = [new PackageFile(parsed.EntryAssembly, bytes.Length, hash)] };
|
|
Assert.True(new PackageManifestValidator().Validate(manifest, 2026, "x64").IsValid);
|
|
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "fixture"); Directory.CreateDirectory(source); await System.IO.File.WriteAllBytesAsync(Path.Combine(source, manifest.EntryAssembly), bytes);
|
|
try { var store = new LocalPackageStore(root, new Sha256FileHasher()); var active = await store.SynchronizeAsync(manifest, source, CancellationToken.None); var packageRoot = Path.Combine(root, "Applications", manifest.PackageId, "versions", active.CurrentVersion); var script = new StartupScriptBuilder().BuildNetloadScript(packageRoot, manifest.EntryAssembly); var launch = new DryRunAutodeskProcessLauncher().Prepare(new LaunchRequest(new("C:\\acad.exe", "2026", "x64", []), "Pilot", script, true)); Assert.False(launch.WouldStart); Assert.Single(System.Text.RegularExpressions.Regex.Matches(script, "netload", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Cast<object>()); }
|
|
finally { Directory.Delete(root, true); }
|
|
}
|
|
[Fact] public void PilotCli_UsesContentAddressedVersionForImmutableActivation()
|
|
{
|
|
var root = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (root is not null && !File.Exists(Path.Combine(root.FullName, "Launcher.Next.sln"))) root = root.Parent;
|
|
var source = File.ReadAllText(Path.Combine(root!.FullName, "Launcher.Next.App", "PilotCli.cs"));
|
|
Assert.Contains("contentIdentity", source);
|
|
Assert.Contains("assemblyVersion + \"-\" + contentIdentity", source);
|
|
}
|
|
}
|