From 43a1976b830c3ae47ebe9114196374b316e875b1 Mon Sep 17 00:00:00 2001 From: Lukas Kurz Date: Tue, 22 Oct 2024 00:01:19 +0200 Subject: [PATCH] Added basic end2end tests --- .../EndpointInfo.cs | 10 +- .../Session/Upgrade/HostUpgradeHandler.cs | 7 +- .../Transports/Network/NetworkTransport.cs | 47 ++++-- .../E2E/BluetoothHandler.cs | 84 ++++++++++ .../E2E/DeviceContainer.cs | 57 +++++++ .../E2E/End2EndTest.cs | 151 ++++++++++++++++++ .../E2E/NetworkHandler.cs | 9 ++ .../E2E/TestLoggerProvider.cs | 27 ++++ 8 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/BluetoothHandler.cs create mode 100644 tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/DeviceContainer.cs create mode 100644 tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/End2EndTest.cs create mode 100644 tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/NetworkHandler.cs create mode 100644 tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/TestLoggerProvider.cs diff --git a/lib/ShortDev.Microsoft.ConnectedDevices/EndpointInfo.cs b/lib/ShortDev.Microsoft.ConnectedDevices/EndpointInfo.cs index fecedaf..f8f0883 100644 --- a/lib/ShortDev.Microsoft.ConnectedDevices/EndpointInfo.cs +++ b/lib/ShortDev.Microsoft.ConnectedDevices/EndpointInfo.cs @@ -23,13 +23,13 @@ public IPEndPoint ToIPEndPoint() } public static EndpointInfo FromTcp(IPEndPoint endpoint) - => FromTcp(endpoint.Address); + => FromTcp(endpoint.Address, endpoint.Port); - public static EndpointInfo FromTcp(IPAddress address) - => FromTcp(address.ToString()); + public static EndpointInfo FromTcp(IPAddress address, int port = Constants.TcpPort) + => FromTcp(address.ToString(), port); - public static EndpointInfo FromTcp(string address) - => new(CdpTransportType.Tcp, address, Constants.TcpPort.ToString()); + public static EndpointInfo FromTcp(string address, int port = Constants.TcpPort) + => new(CdpTransportType.Tcp, address, port.ToString()); public static EndpointInfo FromRfcommDevice(PhysicalAddress macAddress) => new(CdpTransportType.Rfcomm, macAddress.ToStringFormatted(), Constants.RfcommServiceId); diff --git a/lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs b/lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs index 251fb99..5d7f15c 100644 --- a/lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs +++ b/lib/ShortDev.Microsoft.ConnectedDevices/Session/Upgrade/HostUpgradeHandler.cs @@ -95,8 +95,9 @@ void HandleUpgradeRequest(CdpSocket socket, ref EndianReader reader) Type = MessageType.Connect }; - var localIp = _session.Platform.TryGetTransport()?.Handler.TryGetLocalIp(); - if (localIp == null) + var networkTransport = _session.Platform.TryGetTransport(); + var localIp = networkTransport?.Handler.TryGetLocalIp(); + if (networkTransport == null || localIp == null) { EndianWriter writer = new(Endianness.BigEndian); new ConnectionHeader() @@ -126,7 +127,7 @@ void HandleUpgradeRequest(CdpSocket socket, ref EndianReader reader) { Endpoints = [ - EndpointInfo.FromTcp(localIp) + EndpointInfo.FromTcp(localIp, networkTransport.TcpPort) ], MetaData = [ diff --git a/lib/ShortDev.Microsoft.ConnectedDevices/Transports/Network/NetworkTransport.cs b/lib/ShortDev.Microsoft.ConnectedDevices/Transports/Network/NetworkTransport.cs index c0d2a6c..b62cad9 100644 --- a/lib/ShortDev.Microsoft.ConnectedDevices/Transports/Network/NetworkTransport.cs +++ b/lib/ShortDev.Microsoft.ConnectedDevices/Transports/Network/NetworkTransport.cs @@ -6,25 +6,32 @@ namespace ShortDev.Microsoft.ConnectedDevices.Transports.Network; -public sealed class NetworkTransport(INetworkHandler handler) : ICdpTransport, ICdpDiscoverableTransport +public sealed class NetworkTransport( + INetworkHandler handler, + int tcpPort = Constants.TcpPort, int udpPort = Constants.UdpPort +) : ICdpTransport, ICdpDiscoverableTransport { - readonly TcpListener _listener = new(IPAddress.Any, Constants.TcpPort); + public int TcpPort { get; } = tcpPort; + public int UdpPort { get; } = udpPort; + + TcpListener? _listener; public INetworkHandler Handler { get; } = handler; public CdpTransportType TransportType { get; } = CdpTransportType.Tcp; public EndpointInfo GetEndpoint() - => new(TransportType, Handler.GetLocalIp().ToString(), Constants.TcpPort.ToString(CultureInfo.InvariantCulture)); + => new(TransportType, Handler.GetLocalIp().ToString(), TcpPort.ToString(CultureInfo.InvariantCulture)); public event DeviceConnectedEventHandler? DeviceConnected; public async Task Listen(CancellationToken cancellationToken) { - _listener.Start(); + var listener = _listener ??= new(IPAddress.Any, TcpPort); + listener.Start(); try { while (!cancellationToken.IsCancellationRequested) { - var client = await _listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); + var client = await listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false); if (client.Client.RemoteEndPoint is not IPEndPoint endPoint) return; @@ -39,7 +46,7 @@ public async Task Listen(CancellationToken cancellationToken) Endpoint = new EndpointInfo( TransportType, endPoint.Address.ToString(), - Constants.TcpPort.ToString(CultureInfo.InvariantCulture) + TcpPort.ToString(CultureInfo.InvariantCulture) ) }); } @@ -65,10 +72,26 @@ public async Task ConnectAsync(EndpointInfo endpoint) #region Discovery (Udp) - readonly UdpClient _udpclient = new(Constants.UdpPort) + readonly UdpClient _udpclient = CreateUdpClient(udpPort); + + static UdpClient CreateUdpClient(int port) { - EnableBroadcast = true - }; + UdpClient client = new() + { + EnableBroadcast = true + }; + + if (OperatingSystem.IsWindows()) + { + const int SIO_UDP_CONNRESET = -1744830452; + client.Client.IOControl(SIO_UDP_CONNRESET, [0, 0, 0, 0], null); + } + + client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + client.Client.Bind(new IPEndPoint(IPAddress.Any, port)); + + return client; + } public async Task Advertise(LocalDeviceInfo deviceInfo, CancellationToken cancellationToken) { @@ -194,7 +217,7 @@ void SendPresenceRequest() Type = DiscoveryType.PresenceRequest }.Write(payloadWriter); - new UdpFragmentSender(_udpclient, new IPEndPoint(IPAddress.Broadcast, Constants.UdpPort)) + new UdpFragmentSender(_udpclient, new IPEndPoint(IPAddress.Broadcast, UdpPort)) .SendMessage(header, payloadWriter.Buffer.AsSpan()); } @@ -212,7 +235,7 @@ void SendPresenceResponse(IPAddress device, PresenceResponse response) }.Write(payloadWriter); response.Write(payloadWriter); - new UdpFragmentSender(_udpclient, new IPEndPoint(device, Constants.UdpPort)) + new UdpFragmentSender(_udpclient, new IPEndPoint(device, UdpPort)) .SendMessage(header, payloadWriter.Buffer.AsSpan()); } @@ -228,7 +251,7 @@ public void Dispose() DeviceDiscovered = null; DiscoveryMessageReceived = null; - _listener.Dispose(); + _listener?.Dispose(); _udpclient.Dispose(); } } diff --git a/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/BluetoothHandler.cs b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/BluetoothHandler.cs new file mode 100644 index 0000000..95af12d --- /dev/null +++ b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/BluetoothHandler.cs @@ -0,0 +1,84 @@ +using ShortDev.Microsoft.ConnectedDevices.Transports; +using ShortDev.Microsoft.ConnectedDevices.Transports.Bluetooth; +using System.IO.Pipes; +using System.Net.NetworkInformation; + +namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E; + +internal sealed class BluetoothHandler(DeviceContainer container, DeviceContainer.Device device) : IBluetoothHandler +{ + public PhysicalAddress MacAddress => PhysicalAddress.Parse(device.Address); + + public Task ConnectRfcommAsync(EndpointInfo endpoint, RfcommOptions options, CancellationToken cancellationToken = default) + { + var device = container.FindDevice(endpoint.Address) + ?? throw new KeyNotFoundException("Could not find device"); + + return Task.FromResult( + device.ConnectFrom(new(CdpTransportType.Rfcomm, device.Address, options.ServiceId ?? "")) + ); + } + + public async Task ListenRfcommAsync(RfcommOptions options, CancellationToken cancellationToken = default) + { + device.ConnectionRequest += OnNewConnection; + + await cancellationToken.AwaitCancellation(); + + device.ConnectionRequest -= OnNewConnection; + + void OnNewConnection(EndpointInfo client, ref (Stream Input, Stream Output)? clientStream) + { + AnonymousPipeServerStream serverInputStream = new(PipeDirection.In); + AnonymousPipeServerStream serverOutputStream = new(PipeDirection.Out); + + // Accept connection + clientStream = ( + new AnonymousPipeClientStream(PipeDirection.In, serverOutputStream.GetClientHandleAsString()), + new AnonymousPipeClientStream(PipeDirection.Out, serverInputStream.GetClientHandleAsString()) + ); + + options.SocketConnected?.Invoke(new CdpSocket() + { + InputStream = serverInputStream, + OutputStream = serverOutputStream, + Endpoint = client, + Close = () => + { + serverInputStream.Dispose(); + serverOutputStream.Dispose(); + } + }); + } + } + + public async Task AdvertiseBLeBeaconAsync(AdvertiseOptions options, CancellationToken cancellationToken = default) + { + var data = options.BeaconData.ToArray(); + container.Advertise(device, (uint)options.ManufacturerId, data); + + await cancellationToken.AwaitCancellation(); + + container.TryRemove(device); + } + + public async Task ScanBLeAsync(ScanOptions scanOptions, CancellationToken cancellationToken = default) + { + container.FoundDevice += OnNewDevice; + + await cancellationToken.AwaitCancellation(); + + container.FoundDevice -= OnNewDevice; + + void OnNewDevice(DeviceContainer.Device device, DeviceContainer.Adverstisement ad) + { + if (ad.Manufacturer != Constants.BLeBeaconManufacturerId) + return; + + if (!BLeBeacon.TryParse(ad.Data.ToArray(), out var beaconData)) + return; + + scanOptions.OnDeviceDiscovered?.Invoke(beaconData); + } + } +} diff --git a/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/DeviceContainer.cs b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/DeviceContainer.cs new file mode 100644 index 0000000..f9d961d --- /dev/null +++ b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/DeviceContainer.cs @@ -0,0 +1,57 @@ +using ShortDev.Microsoft.ConnectedDevices.Transports; +using System.Collections.Concurrent; + +namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E; + +internal sealed class DeviceContainer +{ + readonly ConcurrentDictionary> _registry = []; + sealed record Entry(Device Device, List Adverstisements); + + public Device? FindDevice(string address) + => _registry.FirstOrDefault(x => x.Key.Address == address).Key; + + public void Advertise(Device device, uint manufacturer, ReadOnlyMemory data) + { + var list = _registry.GetOrAdd(device, static key => []); + lock (list) + { + list.Add(new(manufacturer, data)); + } + FoundDevice?.Invoke(device, new(manufacturer, data)); + } + + public bool TryRemove(Device device) + => _registry.Remove(device, out _); + + public event Action? FoundDevice; + + public sealed record Adverstisement(uint Manufacturer, ReadOnlyMemory Data); + public sealed record Device(CdpTransportType TransportType, string Address) + { + public CdpSocket ConnectFrom(EndpointInfo client) + { + (Stream Input, Stream Output)? stream = null; + ConnectionRequest?.Invoke(client, ref stream); + + if (stream is null) + throw new InvalidOperationException("Server did not accept"); + + return new CdpSocket() + { + InputStream = stream.Value.Input, + OutputStream = stream.Value.Output, + Endpoint = new(TransportType, Address, "Some Service Id"), + Close = () => + { + stream.Value.Input.Dispose(); + stream.Value.Output.Dispose(); + } + }; + } + + public event ConnectionRequestHandler? ConnectionRequest; + + public delegate void ConnectionRequestHandler(EndpointInfo client, ref (Stream Input, Stream Output)? stream); + } +} diff --git a/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/End2EndTest.cs b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/End2EndTest.cs new file mode 100644 index 0000000..b066638 --- /dev/null +++ b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/End2EndTest.cs @@ -0,0 +1,151 @@ +using Microsoft.Extensions.Logging; +using ShortDev.Microsoft.ConnectedDevices.Encryption; +using ShortDev.Microsoft.ConnectedDevices.NearShare; +using ShortDev.Microsoft.ConnectedDevices.Transports.Bluetooth; +using ShortDev.Microsoft.ConnectedDevices.Transports.Network; +using System.Net; +using Xunit.Abstractions; + +namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E; + +public sealed class End2EndTest(ITestOutputHelper outputHelper) +{ + ConnectedDevicesPlatform CreateDevice(DeviceContainer network, string name, string btAddress) + { + LocalDeviceInfo DeviceInfo = new() + { + Name = name, + OemManufacturerName = name, + OemModelName = name, + Type = DeviceType.Linux, + DeviceCertificate = ConnectedDevicesPlatform.CreateDeviceCertificate(CdpEncryptionParams.Default) + }; + + var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddProvider(new TestLoggerProvider(name, outputHelper)); + }); + ConnectedDevicesPlatform cdp = new(DeviceInfo, loggerFactory); + + BluetoothHandler btHandler = new(network, new(Transports.CdpTransportType.Rfcomm, btAddress)); + cdp.AddTransport(new BluetoothTransport(btHandler)); + + return cdp; + } + + static void UseTcp(ConnectedDevicesPlatform cdp, int tcpPort, int udpPort) + { + NetworkHandler networkHandler = new(IPAddress.Loopback); + NetworkTransport networkTransport = new(networkHandler, tcpPort, udpPort); + cdp.AddTransport(networkTransport); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task TransferUri(bool useTcp1, bool useTcp2) + { + DeviceContainer network = new(); + + using var device1 = CreateDevice(network, "Device 1", "57-0C-4A-27-07-52"); + if (useTcp1) + UseTcp(device1, tcpPort: 5041, udpPort: 5051); + + device1.Discover(cancellationToken: default); + + using var device2 = CreateDevice(network, "Device 2", "81-7A-80-8F-D5-80"); + if (useTcp2) + UseTcp(device2, tcpPort: 5041, udpPort: 5051); + + device2.Advertise(cancellationToken: default); + device2.Listen(cancellationToken: default); + + TaskCompletionSource receivePromise = new(); + NearShareReceiver.ReceivedUri += receivePromise.SetResult; + NearShareReceiver.Register(device2); + + try + { + NearShareSender sender = new(device1); + await sender.SendUriAsync( + device: new("Device 2", DeviceType.Linux, Endpoint: + new(Transports.CdpTransportType.Rfcomm, "81-7A-80-8F-D5-80", "ServiceId") + ), new Uri("https://nearshare.shortdev.de/") + ); + + var token = await receivePromise.Task; + Assert.Equal("Device 1", token.DeviceName); + Assert.Equal("https://nearshare.shortdev.de/", token.Uri); + } + finally + { + NearShareReceiver.Unregister(); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task TransferFile(bool useTcp1, bool useTcp2) + { + DeviceContainer network = new(); + + using var device1 = CreateDevice(network, "Device 1", "57-0C-4A-27-07-52"); + if (useTcp1) + UseTcp(device1, tcpPort: 5041, udpPort: 5051); + + device1.Discover(cancellationToken: default); + + using var device2 = CreateDevice(network, "Device 2", "81-7A-80-8F-D5-80"); + if (useTcp2) + UseTcp(device2, tcpPort: 5041, udpPort: 5051); + + device2.Advertise(cancellationToken: default); + device2.Listen(cancellationToken: default); + + var buffer = new byte[Random.Shared.Next(1_000, 1_000_000)]; + outputHelper.WriteLine($"[Information]: Generated buffer with size {buffer.LongLength}"); + Random.Shared.NextBytes(buffer); + + MemoryStream receivedData = new(); + TaskCompletionSource receivePromise = new(); + NearShareReceiver.FileTransfer += OnFileTransfer; + NearShareReceiver.Register(device2); + + try + { + NearShareSender sender = new(device1); + await sender.SendFileAsync( + device: new("Device 2", DeviceType.Linux, Endpoint: + new(Transports.CdpTransportType.Rfcomm, "81-7A-80-8F-D5-80", "ServiceId") + ), + CdpFileProvider.FromBuffer("TestFile", buffer), + new Progress() + ); + + await receivePromise.Task; + + Assert.Equal(buffer, receivedData.ToArray()); + } + finally + { + NearShareReceiver.Unregister(); + } + + void OnFileTransfer(FileTransferToken token) + { + Assert.Equal("Device 1", token.DeviceName); + Assert.Equal(1, token.Files.Count); + Assert.Equal("TestFile", token.Files[0].Name); + Assert.Equal((ulong)buffer.LongLength, token.Files[0].Size); + + token.Accept([receivedData]); + token.Finished += receivePromise.SetResult; + } + } +} diff --git a/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/NetworkHandler.cs b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/NetworkHandler.cs new file mode 100644 index 0000000..ffdf20e --- /dev/null +++ b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/NetworkHandler.cs @@ -0,0 +1,9 @@ +using ShortDev.Microsoft.ConnectedDevices.Transports.Network; +using System.Net; + +namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E; + +internal class NetworkHandler(IPAddress address) : INetworkHandler +{ + public IPAddress GetLocalIp() => address; +} diff --git a/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/TestLoggerProvider.cs b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/TestLoggerProvider.cs new file mode 100644 index 0000000..1733432 --- /dev/null +++ b/tests/ShortDev.Microsoft.ConnectedDevices.Test/E2E/TestLoggerProvider.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace ShortDev.Microsoft.ConnectedDevices.Test.E2E; + +internal sealed class TestLoggerProvider(string deviceName, ITestOutputHelper outputHelper) : ILoggerProvider, ILogger +{ + public ILogger CreateLogger(string categoryName) + => this; + + public IDisposable? BeginScope(TState state) where TState : notnull + => null; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var msg = formatter(state, exception); + if (exception is not null) + msg += '\n' + exception.Message; + + outputHelper.WriteLine($"[{logLevel}]: [{deviceName}]: ({eventId.Name}) {msg}"); + } + + public void Dispose() { } +}