XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.InputSimulator

【DLL】XFE各类拓展的模拟键盘输入

公开
关注 0 Fork 0 Star 0
UTF-8
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<NotSupportedException>(() => driver.SendUnicode('中', false));
        Assert.Throws<NotSupportedException>(() => driver.MoveMouse(100, 100, true, false));
        Assert.Throws<ArgumentException>(() => driver.MoveMouse(1, 1, false, true));
        Assert.Throws<ArgumentOutOfRangeException>(() => driver.ScrollMouse(1, false));
        Assert.Throws<ArgumentOutOfRangeException>(() => driver.SendKey((ScanCode)0xE020, false));
        Assert.Throws<ArgumentOutOfRangeException>(() => 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<OperationCanceledException>(() => task);
        Assert.Equal<IEnumerable<uint>>(new uint[] { 0xE1, 0x1A, 0x1A, 0xE1 }, channel.Commands.Select(command => command.Code));
        Assert.Equal<IEnumerable<uint>>(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<NotSupportedException>(() => 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<IOException>(() => 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<IOException>(driver.Dispose);
        Assert.True(channel.Disposed);
        driver.Dispose();
        Assert.Equal(1, channel.Resets);
        Assert.Throws<ObjectDisposedException>(() => 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<DriverCommand> 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;
    }
}