using System.Security.Cryptography; using System.Text.Json; using XFEExtension.NetCore.InputSimulator.Native; namespace XFEExtension.NetCore.InputSimulator.Tests; [TestFixture] public class DriverBackendTests { [Test] public void KeyButtonAndMovementUseTheDriverProtocol() { var channel = new RecordingDriver(); using var driver = new DriverBackend(channel); driver.SendKey(ScanCode.RightControl, false); driver.SendKey(ScanCode.RightControl, true); driver.SendMouseButton(MouseButton.XButton2, false); driver.SendMouseButton(MouseButton.XButton2, true); driver.MoveMouse(-70000, 32768, false, false); Assert.Equal(7, channel.Commands.Count); Assert.Equal((1u, 0xE4u, 0u), (channel.Commands[0].Kind, channel.Commands[0].Code, channel.Commands[0].Flags)); Assert.Equal(1u, channel.Commands[1].Flags); Assert.Equal((2u, 4u, 0u), (channel.Commands[2].Kind, channel.Commands[2].Code, channel.Commands[2].Flags)); Assert.Equal(1u, channel.Commands[3].Flags); Assert.Equal(-70000, channel.Commands.Skip(4).Sum(command => command.X)); Assert.Equal(32768, channel.Commands.Skip(4).Sum(command => command.Y)); Assert.True(channel.Commands.Skip(4).All(command => command.Kind == 3 && Math.Abs(command.X) <= 32767 && Math.Abs(command.Y) <= 32767)); Assert.True(channel.Commands.All(command => command.Version == 1 && command.Reserved == 0)); } [Test] public void WheelSplitsLargeNotchCountsWithoutLosingSignOrOrientation() { var channel = new RecordingDriver(); using var driver = new DriverBackend(channel); driver.ScrollMouse(-120 * 300, false); driver.ScrollMouse(120 * 128, true); Assert.Equal(5, channel.Commands.Count); Assert.Equal(-300, channel.Commands.Where(command => command.Flags == 0).Sum(command => command.Delta)); Assert.Equal(128, channel.Commands.Where(command => command.Flags == 1).Sum(command => command.Delta)); Assert.True(channel.Commands.All(command => command.Kind == 4 && Math.Abs(command.Delta) <= 127)); driver.ScrollMouse(0, false); Assert.Equal(5, channel.Commands.Count); } [Test] public void UnsupportedInputFailsWithoutSendingAnything() { var channel = new RecordingDriver(); using var driver = new DriverBackend(channel); Assert.False(driver.SupportsUnicodeText); Assert.Throws(() => driver.SendUnicode('δΈ­', false)); Assert.Throws(() => driver.MoveMouse(100, 100, true, false)); Assert.Throws(() => driver.MoveMouse(1, 1, false, true)); Assert.Throws(() => driver.ScrollMouse(1, false)); Assert.Throws(() => driver.SendKey((ScanCode)0xE020, false)); Assert.Throws(() => driver.SendMouseButton((MouseButton)5, false)); Assert.Empty(channel.Commands); } [Test] public async Task CancellingDriverCombinationReleasesKeysInReverseOrder() { var channel = new RecordingDriver(); using var input = new InputController(new DriverBackend(channel)); using var cancellation = new CancellationTokenSource(); var task = input.PressCombinationAsync([ScanCode.LeftShift, ScanCode.W], 10000, cancellation.Token); cancellation.Cancel(); await Assert.ThrowsAsync(() => task); Assert.Equal>(new uint[] { 0xE1, 0x1A, 0x1A, 0xE1 }, channel.Commands.Select(command => command.Code)); Assert.Equal>(new uint[] { 0, 0, 1, 1 }, channel.Commands.Select(command => command.Flags)); } [Test] public void ProtocolMismatchDisposesTheConnection() { var channel = new RecordingDriver { Info = new() { Version = 99 } }; Assert.Throws(() => new DriverBackend(channel)); Assert.True(channel.Disposed); Assert.Empty(channel.Commands); } [Test] public void FailedSendStopsSplitMovementAndStillAllowsCleanup() { var channel = new RecordingDriver { FailOnCommand = 2 }; using (var driver = new DriverBackend(channel)) Assert.Throws(() => driver.MoveMouse(100000, 0, false, false)); Assert.Equal(2, channel.Commands.Count); Assert.Equal(1, channel.Resets); Assert.True(channel.Disposed); } [Test] public void DisposalClosesHandleEvenWhenResetFails() { var channel = new RecordingDriver { FailReset = true }; var driver = new DriverBackend(channel); Assert.Throws(driver.Dispose); Assert.True(channel.Disposed); driver.Dispose(); Assert.Equal(1, channel.Resets); Assert.Throws(() => driver.SendKey(ScanCode.W, false)); } [Test] public void PayloadIsEmbeddedInDllAndEveryResourceMatchesItsManifest() { Assert.True(DriverDeployment.HasEmbeddedDriver); var assembly = typeof(DriverDeployment).Assembly; using var stream = assembly.GetManifestResourceStream("XFE.InputDriver.payload.json")!; using var manifest = JsonDocument.Parse(stream); Assert.Equal(1, manifest.RootElement.GetProperty("protocol").GetInt32()); Assert.Equal(DriverDeployment.IsEmbeddedDriverSigned ? 5 : 4, manifest.RootElement.GetProperty("files").EnumerateObject().Count()); Assert.True(manifest.RootElement.GetProperty("files").TryGetProperty("XfeInputDriver.dll", out _)); Assert.Equal(manifest.RootElement.GetProperty("timestamped").GetBoolean(), DriverDeployment.IsEmbeddedDriverTimestamped); if (DriverDeployment.IsEmbeddedDriverSigned) { using var certificateStream = assembly.GetManifestResourceStream("XFE.InputDriver.publisher.cer")!; using var bytes = new MemoryStream(); certificateStream.CopyTo(bytes); using var certificate = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadCertificate(bytes.ToArray()); Assert.False(certificate.HasPrivateKey); Assert.Equal(new DateTimeOffset(certificate.NotAfter.ToUniversalTime()), DriverDeployment.EmbeddedDriverCertificateExpires!.Value); } foreach (var file in manifest.RootElement.GetProperty("files").EnumerateObject()) { using var content = assembly.GetManifestResourceStream("XFE.InputDriver." + file.Name)!; Assert.Equal(file.Value.GetString(), Convert.ToHexString(SHA256.HashData(content))); } } private sealed class RecordingDriver : IDriverTransport { internal DriverInfo Info = new() { Version = 1, Capabilities = 31, MaxKeys = 6 }; internal List Commands { get; } = []; internal int Resets; internal bool Disposed, FailReset; internal int FailOnCommand; public DriverInfo Query() => Info; public void Send(DriverCommand command) { Commands.Add(command); if (Commands.Count == FailOnCommand) throw new IOException("test send failure"); } public void Reset() { Resets++; if (FailReset) throw new IOException("test reset failure"); } public void Dispose() => Disposed = true; } }