Files
launcher-next/tests/Launcher.Next.Tests/PackageResolutionUseCaseTests.cs

61 lines
6.7 KiB
C#

using System.Security.Cryptography;
using Launcher.Next.Application;
using Launcher.Next.Core;
using Launcher.Next.Infrastructure;
using Xunit;
namespace Launcher.Next.Tests;
public sealed class PackageResolutionUseCaseTests
{
private static PackageRequirement Requirement(string version, bool offline = true, string product = "Plant3D", int year = 2026, string architecture = "x64", string channel = "pilot", string roster = "project-a") => new("P_Rec_Annot", product, year, architecture, channel, version, roster, offline);
private static PackageManifest Manifest(string version, byte[] bytes, string product = "Plant3D", int year = 2026, string architecture = "x64", string channel = "pilot") => new(1, "P_Rec_Annot", version, product, year, architecture, "PRecAnnot.Plant3D2026.dll", [new("PRecAnnot.Plant3D2026.dll", bytes.Length, Convert.ToHexString(SHA256.HashData(bytes)))], new("netload", "PHP_REC_ANNOT"), RolloutChannel: channel);
[Fact] public async Task OnlineSuccess_StagesBindsAndRecordsOnlineEvidence()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var bytes = new byte[] { 1, 2 }; await File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var manifest = Manifest("1.0.0", bytes); var store = new LocalPackageStore(root, new Sha256FileHasher());
try { var result = await UseCase(new(new CatalogPackage(manifest, source))).ResolveAsync(Requirement("1.0.0"), CancellationToken.None); Assert.True(result.IsResolved); Assert.True(result.Online); Assert.False(result.Selected!.IsOfflineFallback); Assert.Contains(result.Evidence, x => x.Code == "ONLINE_PACKAGE_SELECTED"); Assert.False(result.Selected.LocalPackageRoot.StartsWith("\\\\", StringComparison.Ordinal)); }
finally { Directory.Delete(root, true); }
PackageResolutionUseCase UseCase(FakeCatalog catalog) => new(catalog, store, store, store, new Telemetry());
}
[Fact] public async Task ControlledOfflineRecovery_CorruptB_SelectsVerifiedAAndEvidence()
{
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
{
var a = new byte[] { 1 }; await File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), a); await store.SynchronizeAsync(Manifest("1.0.0", a), source, CancellationToken.None); await store.BindAsync(Requirement("1.0.0"), "1.0.0", CancellationToken.None);
var b = new byte[] { 2 }; await File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), b); await store.SynchronizeAsync(Manifest("2.0.0", b), source, CancellationToken.None); await store.BindAsync(Requirement("2.0.0"), "2.0.0", CancellationToken.None);
await File.WriteAllBytesAsync(Path.Combine(root, "Applications", "P_Rec_Annot", "versions", "2.0.0", "PRecAnnot.Plant3D2026.dll"), new byte[] { 9 });
var result = await new PackageResolutionUseCase(new FakeCatalog(null), store, store, store, new Telemetry()).ResolveAsync(Requirement("1.0.0"), CancellationToken.None);
Assert.True(result.IsResolved); Assert.False(result.Online); Assert.Equal("1.0.0", result.Selected!.Version); Assert.True(result.Selected.IsOfflineFallback); Assert.Contains(result.Evidence, x => x.Code == "PREVIOUS_LKG_SELECTED"); Assert.Contains(result.Evidence, x => x.Code == "CATALOG_UNAVAILABLE");
}
finally { Directory.Delete(root, true); }
}
[Fact] public async Task UnavailableCatalog_WithFallbackDisabled_BlocksWithoutSelectingPrevious()
{
var store = new LocalPackageStore(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), new Sha256FileHasher());
var result = await new PackageResolutionUseCase(new FakeCatalog(null), store, store, store, new Telemetry()).ResolveAsync(Requirement("1.0.0", false), CancellationToken.None);
Assert.False(result.IsResolved); Assert.Contains(result.Evidence, x => x.Code == "OFFLINE_FALLBACK_DISABLED"); Assert.Contains(result.Evidence, x => x.Code == "NO_SAFE_PACKAGE_AVAILABLE");
}
[Fact] public async Task OnlineProductMismatch_IsRejectedAndBlocked()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var source = Path.Combine(root, "source"); Directory.CreateDirectory(source); var bytes = new byte[] { 1 }; await File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var store = new LocalPackageStore(root, new Sha256FileHasher());
try { var result = await new PackageResolutionUseCase(new FakeCatalog(new CatalogPackage(Manifest("1.0.0", bytes, "Revit"), source)), store, store, store, new Telemetry()).ResolveAsync(Requirement("1.0.0"), CancellationToken.None); Assert.False(result.IsResolved); Assert.Contains(result.Evidence, x => x.Code == "NO_SAFE_PACKAGE_AVAILABLE"); }
finally { Directory.Delete(root, true); }
}
[Theory]
[InlineData("Revit", 2026, "x64", "pilot", "1.0.0", "project-a")]
[InlineData("Plant3D", 2025, "x64", "pilot", "1.0.0", "project-a")]
[InlineData("Plant3D", 2026, "x86", "pilot", "1.0.0", "project-a")]
[InlineData("Plant3D", 2026, "x64", "stable", "1.0.0", "project-a")]
[InlineData("Plant3D", 2026, "x64", "pilot", "2.0.0", "project-a")]
[InlineData("Plant3D", 2026, "x64", "pilot", "1.0.0", "project-b")]
public async Task CachedPackage_RejectsIncompatibleRequirement(string product, int year, string architecture, string channel, string version, string roster)
{
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 File.WriteAllBytesAsync(Path.Combine(source, "PRecAnnot.Plant3D2026.dll"), bytes); var store = new LocalPackageStore(root, new Sha256FileHasher());
try { await store.SynchronizeAsync(Manifest("1.0.0", bytes), source, CancellationToken.None); await store.BindAsync(Requirement("1.0.0"), "1.0.0", CancellationToken.None); Assert.Null(await store.ResolveAsync(Requirement(version, true, product, year, architecture, channel, roster), CancellationToken.None)); }
finally { Directory.Delete(root, true); }
}
private sealed class FakeCatalog(CatalogPackage? package) : IPackageCatalogPackageAcquirer { public Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken) => Task.FromResult(package); }
private sealed class Telemetry : ILauncherTelemetry { public void Write(string eventType, IReadOnlyDictionary<string, object?>? fields = null) { } }
}