using Launcher.Next.Application; using Launcher.Next.Core; using Launcher.Next.Infrastructure; using System.Security.Cryptography; using Xunit; namespace Launcher.Next.Tests; public sealed class RosterAdaptersTests { private static readonly RosterContext Context = new("Plant3D", 2026, "x64", true, "legacy-project-a"); [Fact] public void ProjectRoster_ProducesDeterministicRequirements() { var selected = new RosterAdapter().Select(Json(), null, null, Context); var requirements = new RosterAdapter().Requirements(selected.Roster!); Assert.Equal(RosterSource.ProjectRosterJson, selected.Source); Assert.Equal(["P_A", "P_B"], requirements.Select(x => x.PackageId)); Assert.Equal(["1.0.0", "2.0.0"], requirements.Select(x => x.RequiredVersion)); } [Fact] public void LegacyDownloadList_PreservesSourceOrderAndUsesContext() { const string xml = ""; var selected = new RosterAdapter().Select(null, xml, null, Context); Assert.Equal(RosterSource.LegacyDownloadListXml, selected.Source); Assert.Equal("legacy-project-a", selected.Roster!.RosterId); Assert.Equal(["P_First", "P_Second"], selected.Roster.Packages.Select(x => x.Id)); Assert.False(selected.Roster.Packages[1].Enabled); } [Fact] public void ProjectRoster_HasPriorityOverLegacy() { var selected = new RosterAdapter().Select(Json(), "", null, Context); Assert.Equal(RosterSource.ProjectRosterJson, selected.Source); } [Fact] public void MalformedProjectRoster_DoesNotFallBackImplicitly() { Assert.ThrowsAny(() => new RosterAdapter().Select("{", "", Json(), Context)); } [Fact] public void CentralDefault_IsUsedOnlyWhenProjectAndLegacyAreAbsent() { var selected = new RosterAdapter().Select(null, null, Json(), Context); Assert.Equal(RosterSource.CentralDefaultRoster, selected.Source); Assert.Equal("CENTRAL_DEFAULT_ROSTER", selected.Code); } [Fact] public void DuplicatePackage_IsRejected() { const string json = """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_A","version":"1.0.0","channel":"stable"},{"id":"p_a","version":"2.0.0","channel":"stable"}]}"""; var ex = Assert.Throws(() => new RosterAdapter().ParseJson(json)); Assert.Equal("ROSTER_DUPLICATE_PACKAGE", ex.Message); } [Fact] public void LegacyXml_RejectsDtd() { const string xml = "]>"; Assert.ThrowsAny(() => new RosterAdapter().ParseLegacyDownloadList(xml, Context)); } [Fact] public async Task FileSystemCatalog_ReturnsDescriptorOnly_ForMatchingRosterRequirement() { var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var packageRoot=Path.Combine(root,"P_A","1.0.0"); Directory.CreateDirectory(packageRoot); try { var bytes=new byte[]{1}; await File.WriteAllBytesAsync(Path.Combine(packageRoot,"entry.dll"),bytes); var hash=Convert.ToHexString(SHA256.HashData(bytes)); await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), $$"""{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"entry.dll","length":1,"sha256":"{{hash}}"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}"""); var result=await new FileSystemCatalogPackageAcquirer(root).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None); Assert.NotNull(result); Assert.Equal(packageRoot,result!.SourceRoot); Assert.False(result.SourceRoot.StartsWith(@"\\",StringComparison.Ordinal)); } finally { Directory.Delete(root,true); } } private static string Json() => """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_B","version":"2.0.0","channel":"stable","loadOrder":200},{"id":"P_A","version":"1.0.0","channel":"stable","loadOrder":100}]}"""; }