Files
launcher-next/tests/Launcher.Next.Tests/PackageLifecycleTests.cs
T

80 lines
7.6 KiB
C#

using System.Security.Cryptography;
using Launcher.Next.Core;
using Launcher.Next.Infrastructure;
using Xunit;
namespace Launcher.Next.Tests;
public sealed class PackageLifecycleTests
{
private static PackageRequirement Req(string version) => new("Lifecycle.Package", "Plant3D", 2026, "x64", "pilot", version, "lifecycle-test", true);
private static PackageManifest Manifest(string version, byte[] a, byte[] b) => new(1, "Lifecycle.Package", version, "Plant3D", 2026, "x64", "entry.dll", [
new("entry.dll", a.Length, Convert.ToHexString(SHA256.HashData(a))),
new("payload.dat", b.Length, Convert.ToHexString(SHA256.HashData(b)))], new("none", ""), RolloutChannel: "pilot");
[Fact]
public async Task MutatedImmutablePayloadBlocksActivation()
{
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var source=Path.Combine(root,"catalog"); Directory.CreateDirectory(source); var bytes=new byte[]{1}; await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),bytes); await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),bytes); var store=new LocalPackageStore(root,new Sha256FileHasher());
try { var package=await store.MaterializeAsync(Manifest("1.0.0",bytes,bytes),source,CancellationToken.None); await File.WriteAllBytesAsync(Path.Combine(package.LocalPackageRoot,"entry.dll"),new byte[]{9}); Assert.Equal("Blocked",(await store.ActivateImmutableAsync(package,Req("1.0.0"),CancellationToken.None)).Outcome); }
finally { if(Directory.Exists(root)) Directory.Delete(root,true); }
}
[Fact]
public async Task ActivationReceiptBindsPointerGenerationAndRejectsStaleRollback()
{
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var source=Path.Combine(root,"catalog"); Directory.CreateDirectory(source); var bytes=new byte[]{7}; await File.WriteAllBytesAsync(Path.Combine(source,"entry.dll"),bytes); await File.WriteAllBytesAsync(Path.Combine(source,"payload.dat"),bytes); var store=new LocalPackageStore(root,new Sha256FileHasher());
try { var a=await store.MaterializeAsync(Manifest("1.0.0",bytes,bytes),source,CancellationToken.None); await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None); var b=await store.MaterializeAsync(Manifest("2.0.0",bytes,bytes),source,CancellationToken.None); var receipt= (await store.ActivateImmutableAsync(b,Req("2.0.0"),CancellationToken.None)).Receipt!; Assert.True(receipt.PointerGeneration>0); var c=await store.MaterializeAsync(Manifest("3.0.0",bytes,bytes),source,CancellationToken.None); await store.ActivateImmutableAsync(c,Req("3.0.0"),CancellationToken.None); Assert.Equal("RollbackBlockedStateChanged",(await store.RollbackAsync(receipt,CancellationToken.None)).Outcome); }
finally { if(Directory.Exists(root)) Directory.Delete(root,true); }
}
[Fact]
public async Task OwnedCleanupNeverDeletesForeignSibling()
{
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var staging=Path.Combine(root,"Launcher","staging"); var owned=Path.Combine(staging,"owned"); var foreign=Path.Combine(staging,"foreign"); Directory.CreateDirectory(owned); Directory.CreateDirectory(foreign); var ownership=new PackageStagingOwnership("owned","P","1",owned,DateTimeOffset.UtcNow,"owned"); await File.WriteAllTextAsync(Path.Combine(owned,".package-staging-owner.json"),System.Text.Json.JsonSerializer.Serialize(ownership)); var store=new LocalPackageStore(root,new Sha256FileHasher());
try { Assert.Equal("Cleaned",(await store.CleanupOwnedStagingAsync(ownership,CancellationToken.None)).Outcome); Assert.True(Directory.Exists(foreign)); Assert.Equal("AlreadyAbsent",(await store.CleanupOwnedStagingAsync(ownership,CancellationToken.None)).Outcome); }
finally { if(Directory.Exists(root)) Directory.Delete(root,true); }
}
[Fact]
public async Task MaterializeDoesNotCreatePointersAndReturnsImmutableVersion()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "catalog"); Directory.CreateDirectory(source);
var a = new byte[] { 1, 2 }; var b = new byte[] { 3, 4, 5 }; await File.WriteAllBytesAsync(Path.Combine(source, "entry.dll"), a); await File.WriteAllBytesAsync(Path.Combine(source, "payload.dat"), b);
var store = new LocalPackageStore(root, new Sha256FileHasher());
try
{
var result = await store.MaterializeAsync(Manifest("1.0.0", a, b), source, CancellationToken.None);
Assert.True(File.Exists(Path.Combine(result.LocalPackageRoot, "package.manifest.json")));
Assert.False(File.Exists(Path.Combine(root, "Applications", "Lifecycle.Package", "current.json")));
Assert.False(File.Exists(Path.Combine(root, "Applications", "Lifecycle.Package", "previous.json")));
Assert.DoesNotContain("staging", result.LocalPackageRoot, StringComparison.OrdinalIgnoreCase);
}
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
}
[Fact]
public async Task ActivationIsIdempotentAndRollbackRestoresPrevious()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "catalog"); Directory.CreateDirectory(source); var store = new LocalPackageStore(root, new Sha256FileHasher());
var a = new byte[] { 1 }; var b = new byte[] { 2 }; var c = new byte[] { 3 }; await File.WriteAllBytesAsync(Path.Combine(source, "entry.dll"), a); await File.WriteAllBytesAsync(Path.Combine(source, "payload.dat"), b);
try
{
var pa = await store.MaterializeAsync(Manifest("1.0.0", a, b), source, CancellationToken.None); var aa = await store.ActivateImmutableAsync(pa, Req("1.0.0"), CancellationToken.None); Assert.Equal("Activated", aa.Outcome);
var again = await store.ActivateImmutableAsync(pa, Req("1.0.0"), CancellationToken.None); Assert.Equal("AlreadyActiveExact", again.Outcome);
await File.WriteAllBytesAsync(Path.Combine(source, "entry.dll"), c); var pb = await store.MaterializeAsync(Manifest("2.0.0", c, b), source, CancellationToken.None); var ab = await store.ActivateImmutableAsync(pb, Req("2.0.0"), CancellationToken.None); Assert.Equal("Activated", ab.Outcome);
var rollback = await store.RollbackAsync(ab.Receipt!, CancellationToken.None); Assert.Equal("RolledBack", rollback.Outcome); Assert.Equal("1.0.0", await store.GetCurrentVersionAsync("Lifecycle.Package", CancellationToken.None));
}
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
}
[Fact]
public async Task OldReceiptCannotRollbackNewerActivation()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "catalog"); Directory.CreateDirectory(source); var store = new LocalPackageStore(root, new Sha256FileHasher());
var bytes = new byte[] { 1 }; await File.WriteAllBytesAsync(Path.Combine(source, "entry.dll"), bytes); await File.WriteAllBytesAsync(Path.Combine(source, "payload.dat"), bytes);
try { var a=await store.MaterializeAsync(Manifest("1.0.0",bytes,bytes),source,CancellationToken.None); await store.ActivateImmutableAsync(a,Req("1.0.0"),CancellationToken.None); var b=await store.MaterializeAsync(Manifest("2.0.0",bytes,bytes),source,CancellationToken.None); var rb=await store.ActivateImmutableAsync(b,Req("2.0.0"),CancellationToken.None); var r=await store.RollbackAsync(rb.Receipt!,CancellationToken.None); Assert.Equal("RolledBack",r.Outcome); }
finally { if (Directory.Exists(root)) Directory.Delete(root, true); }
}
}