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<Task> 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<PluginPackage>(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<PluginPackage>(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<PluginPackage>(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);
}
}
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<Task> 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<PluginPackage>(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<PluginPackage>(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<PluginPackage>(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);
}
}