using System.Security.Cryptography; using System.Text.Json; using XFEExtension.NetCore.XUnit.Attributes; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Plugins; namespace XFEEDToolBox.Test; [TestFixture] public class PluginTests { [Test] public async Task HandshakeRetriesDroppedLegacyReplyAndKeepsCapabilitiesFalse() { var id=Random.Shared.Next(10000000,19000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server=Task.Run(async()=> { for(var attempt=0;attempt<3;attempt++) { await using var pipe=new System.IO.Pipes.NamedPipeServerStream($"XFEEDToolBox.Plugin.{id}",System.IO.Pipes.PipeDirection.InOut,1,System.IO.Pipes.PipeTransmissionMode.Byte,System.IO.Pipes.PipeOptions.Asynchronous); await pipe.WaitForConnectionAsync(timeout.Token);var buffer=new byte[128];await pipe.ReadAsync(buffer,timeout.Token); if(attempt<2)continue; await pipe.WriteAsync(System.Text.Encoding.UTF8.GetBytes($"{{\"schema\":1,\"name\":\"XFEEDToolBox.Plugin\",\"processId\":{id},\"loaded\":true,\"active\":true}}\n"),timeout.Token); pipe.WaitForPipeDrain(); } },timeout.Token); var result=await GamePluginManager.ProbeAsync(id,token:timeout.Token);await server; Check.That(result.Loaded&&result.Active&&!result.SupportsNavigation&&!result.SupportsTrade&&!result.SupportsRelog); } private sealed class Fixture:IDisposable { public readonly TemporaryDirectory Directory=new();public string Game{get;}public string Loader{get;}public GamePluginManager Manager{get;} public Fixture() { Game=Directory.File("EliteDangerous64.exe");Loader=Directory.File("dinput8.dll");File.WriteAllText(Game,"owned test game image"); var package=Directory.File("package");System.IO.Directory.CreateDirectory(package);var dll=Path.Combine(package,"dinput8.dll");File.WriteAllText(dll,"owned test plugin"); var p=new PluginPackage{Id=GamePluginManager.PluginId,Version="test",Loader="dinput8.dll",Sha256=Hash(dll),SupportedGameSha256=Hash(Game)}; File.WriteAllText(Path.Combine(package,"package.json"),JsonSerializer.Serialize(p,ToolboxSettings.JsonOptions));Manager=new(package); } public void Dispose()=>Directory.Dispose(); private static string Hash(string path)=>Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path))); } private static async Task Fails(Func action){try{await action();}catch(InvalidOperationException){return;}throw new InvalidOperationException("Expected refusal");} [Test] public async Task InstallAndUninstallPreserveChineseComponentAndGameImage() { using var f=new Fixture();var chinese=f.Directory.File("dbghelp.dll");File.WriteAllText(chinese,"existing Chinese component");var before=File.ReadAllText(f.Game); Check.That((await f.Manager.InstallAsync(f.Game)).Installed);Check.That((await f.Manager.UninstallAsync(f.Game)).Installed==false); Check.Equal("existing Chinese component",File.ReadAllText(chinese));Check.Equal(before,File.ReadAllText(f.Game));Check.That(!File.Exists(f.Loader)); } [Test] public async Task ExistingThirdPartyLoaderIsNeverOverwritten() { using var f=new Fixture();File.WriteAllText(f.Loader,"other companion");await Fails(()=>f.Manager.InstallAsync(f.Game));Check.Equal("other companion",File.ReadAllText(f.Loader)); } [Test] public async Task OwnedPluginCanUpdateAndLockedUpdatePreservesInstallation() { using var f=new Fixture();await f.Manager.InstallAsync(f.Game); var package=f.Directory.File("package");var dll=Path.Combine(package,"dinput8.dll"); File.WriteAllText(dll,"updated owned test plugin"); var data=JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(package,"package.json")),ToolboxSettings.JsonOptions)!; data=data with{Version="next",Sha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(dll)))}; File.WriteAllText(Path.Combine(package,"package.json"),JsonSerializer.Serialize(data,ToolboxSettings.JsonOptions)); using(var held=new FileStream(f.Loader,FileMode.Open,FileAccess.Read,FileShare.ReadWrite)) {await Fails(()=>f.Manager.InstallAsync(f.Game));Check.Equal("owned test plugin",File.ReadAllText(f.Loader));Check.Equal("test",(await f.Manager.InspectAsync(f.Game)).Installation!.Version);} var updated=await f.Manager.InstallAsync(f.Game);Check.Equal("next",updated.Installation!.Version);Check.Equal("updated owned test plugin",File.ReadAllText(f.Loader)); Check.Equal("owned test game image",File.ReadAllText(f.Game)); } [Test] public async Task ModifiedOwnedFileIsPreservedOnUninstall() { using var f=new Fixture();await f.Manager.InstallAsync(f.Game);File.WriteAllText(f.Loader,"changed by user");await Fails(()=>f.Manager.UninstallAsync(f.Game));Check.Equal("changed by user",File.ReadAllText(f.Loader)); } [Test] public async Task VerifiedGameUpdateRefreshesInstallationHashWithEitherChangedOrUnchangedLoader() { using var f=new Fixture();await f.Manager.InstallAsync(f.Game); var package=f.Directory.File("package");var manifest=Path.Combine(package,"package.json");var dll=Path.Combine(package,"dinput8.dll"); for(var update=0;update<2;update++) { File.WriteAllText(f.Game,"updated game build "+update); if(update==0)File.WriteAllText(dll,"new compatible loader"); var data=JsonSerializer.Deserialize(File.ReadAllText(manifest),ToolboxSettings.JsonOptions)!; data=data with{Version="new-build-"+update,SupportedGameSha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(f.Game))),Sha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(dll)))}; File.WriteAllText(manifest,JsonSerializer.Serialize(data,ToolboxSettings.JsonOptions)); var actual=await f.Manager.InstallAsync(f.Game); Check.Equal(data.SupportedGameSha256,actual.Installation!.GameSha256);Check.Equal(data.Version,actual.Installation.Version); Check.Equal(data.Sha256,actual.Installation.LoaderSha256);Check.That(actual.Installed); } } [Test] public async Task OwnedUpdateRetriesTheShortMappedFileLockAfterProcessExit() { using var f=new Fixture();await f.Manager.InstallAsync(f.Game); var package=f.Directory.File("package");var dll=Path.Combine(package,"dinput8.dll");File.WriteAllText(dll,"replacement after exit"); var data=JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(package,"package.json")),ToolboxSettings.JsonOptions)!; data=data with{Version="after-exit",Sha256=Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(dll)))}; File.WriteAllText(Path.Combine(package,"package.json"),JsonSerializer.Serialize(data,ToolboxSettings.JsonOptions)); var held=new FileStream(f.Loader,FileMode.Open,FileAccess.Read,FileShare.ReadWrite); var release=Task.Run(async()=>{await Task.Delay(400);held.Dispose();}); try { var updated=await f.Manager.InstallAsync(f.Game); Check.Equal("after-exit",updated.Installation!.Version);Check.Equal("replacement after exit",File.ReadAllText(f.Loader)); Check.That(!File.Exists(f.Directory.File(".xfeedtoolbox-plugin/loader.previous"))); } finally{held.Dispose();await release;} } [Test] public async Task LoadedFileCanBeDisabledAndCleanupCompletedLater() { using var f=new Fixture();await f.Manager.InstallAsync(f.Game); using(var held=new FileStream(f.Loader,FileMode.Open,FileAccess.Read,FileShare.ReadWrite)) {var state=await f.Manager.UninstallAsync(f.Game);Check.That(state.PendingRemoval);Check.That(File.Exists(f.Directory.File(".xfeedtoolbox-plugin/disabled")));} var cleaned=await f.Manager.UninstallAsync(f.Game);Check.That(!cleaned.Installed&&!cleaned.PendingRemoval); } }